diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 991703ac12..c084976f5c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,6 +13,7 @@ - [ ] New third-party code is a submodule built from source - [ ] Generic behaviour, not specific to one title - [ ] Depends on: +- [ ] Fixes: - [ ] Unblocks: - [ ] GPUs tested: - [ ] AI-assisted: diff --git a/.github/workflows/area-labels.yml b/.github/workflows/area-labels.yml index bd3d6a370d..f76171b143 100644 --- a/.github/workflows/area-labels.yml +++ b/.github/workflows/area-labels.yml @@ -26,7 +26,7 @@ jobs: 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 \ + gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 1000 --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" \ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 328669a882..200e825e3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,13 +22,18 @@ on: required: false default: true type: boolean + select: + description: Build and run only the tests whose build inputs the pull requests change (push builds of main always run every test) + required: false + default: false + type: boolean permissions: contents: read concurrency: group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} + cancel-in-progress: true env: CONFIGURE: >- @@ -65,17 +70,70 @@ jobs: const { owner, repo } = context.repo; const workflow = '.github/workflows/build.yml'; const libc = 'core/libs/prx/libc/'; + const jobOfLine = (text) => { + let inJobs = false; + let job = ''; + return text.split('\n').map((line) => { + if (/^\S/.test(line)) { + inJobs = /^jobs:\s*$/.test(line); + job = ''; + } else if (inJobs) { + job = /^ ([\w-]+):\s*$/.exec(line)?.[1] ?? job; + } + return job; + }); + }; + const patchedJobs = (patch, text) => { + const jobs = jobOfLine(text); + const touched = new Set(); + let line = 1; + for (const row of patch.split('\n')) { + const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(row); + if (hunk) { + line = Number(hunk[1]); + } else if (row.startsWith('+')) { + touched.add(jobs[line - 1] ?? ''); + line += 1; + } else if (row.startsWith('-')) { + touched.add(jobs[line - 2] ?? '').add(jobs[line - 1] ?? ''); + } else if (row.startsWith(' ')) { + line += 1; + } + } + return touched; + }; + const workflowJobs = new Set(); + const readWorkflow = async (files, ref) => { + const file = files.find((file) => file.filename === workflow || file.previous_filename === workflow); + if (!file) { + return; + } + if (file.filename !== workflow || !file.patch) { + workflowJobs.add('*'); + return; + } + try { + const { data } = await github.rest.repos.getContent({ owner, repo, path: workflow, ref }); + const text = Buffer.from(data.content, 'base64').toString('utf8'); + patchedJobs(file.patch, text).forEach((job) => workflowJobs.add(job)); + } catch (error) { + core.warning(`Could not read ${workflow} at ${ref}, so every test job runs: ${error.message}`); + workflowJobs.add('*'); + } + }; const select = (paths, all) => { const touches = (...prefixes) => all || paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); + const edits = (job) => ['*', '', 'eligibility', job].some((name) => workflowJobs.has(name)); return { - relinker: touches( + relinker: edits('relinker') || touches( 'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp', 'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp', - 'tools/', 'cmake/', 'CMakeLists.txt', workflow + 'tools/', 'cmake/', 'CMakeLists.txt' ), - registerContext: touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow), - writeWatch: touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow), + registerContext: edits('register-context') || + touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc), + writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc), }; }; const relinkerHosts = (full, linux) => JSON.stringify([ @@ -97,6 +155,7 @@ jobs: const files = data.files ?? []; all = files.length >= 300; paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); + await readWorkflow(files, context.sha); } catch (error) { core.warning(`Could not list the files of this push, so every test job runs: ${error.message}`); } @@ -162,6 +221,7 @@ jobs: const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); truncated ||= files.length >= 3000; paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); + await readWorkflow(files, pr.head.sha); pulls.push({ number: pull_number, head: pr.head.sha }); } const run = select(paths, truncated); @@ -256,7 +316,36 @@ jobs: run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON - run: cmake --build build-relinker --parallel 3 - - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 + - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4 + + relinker-asan: + needs: eligibility + if: ${{ needs.eligibility.outputs.relinker == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done + - run: cmake -S . -B build-relinker-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DANYPS5_RELINKER_ONLY=ON -DANYPS5_ENABLE_ASAN=ON -DBUILD_TESTING=ON + - run: cmake --build build-relinker-asan --parallel 4 + - run: ctest --test-dir build-relinker-asan --output-on-failure --timeout 120 --label-regex '^tls_function_coverage$' --no-tests=error register-context: needs: eligibility @@ -270,6 +359,25 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + shell: bash + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} - run: cmake --build build/register-context --parallel - run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error @@ -285,6 +393,25 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + shell: bash + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release - run: cmake --build build/write-watch --parallel - run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error @@ -300,8 +427,6 @@ jobs: CCACHE_COMPILERCHECK: content MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa MESA_SHADER_CACHE_MAX_SIZE: 4G - CMAKE_BUILD_PARALLEL_LEVEL: 1 - CTEST_PARALLEL_LEVEL: 1 steps: - uses: actions/checkout@v7 with: @@ -335,16 +460,87 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON + - name: Install the wild linker + shell: bash + env: + WILD_VERSION: 0.10.0 + WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283 + run: | + tools=$RUNNER_TOOL_CACHE/wild + mkdir -p "$tools" 2>/dev/null && test -w "$tools" || { tools=$RUNNER_TEMP/wild; mkdir -p "$tools"; } + dir=$tools/$WILD_VERSION + tmp= + probe=$(mktemp -d "$RUNNER_TEMP/wild-probe.XXXXXX") + trap 'rm -rf "$tmp" "$probe"' EXIT + if [ ! -x "$dir/bin/ld" ]; then + tmp=$(mktemp -d "$tools/.wild.XXXXXX") + name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu + curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz" + echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c - + tar -xzf "$tmp/$name.tar.gz" -C "$tmp" + mkdir -p "$tmp/out/bin" + mv "$tmp/$name/wild" "$tmp/out/bin/wild" + ln -s wild "$tmp/out/bin/ld" + mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld" + fi + echo 'int main(void) { return 0; }' > "$probe/main.c" + gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main" + readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION" + echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV" + - name: Size parallelism to what the shared host has free + run: | + cpus=$(nproc) + busy=$(awk '{ printf "%d", $1 + 0.5 }' /proc/loadavg) + mem_mb=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo) + slots=$(( (mem_mb - 2048) / 1024 )) + slots=$(( slots < 1 ? 1 : slots )) + tests=$(( cpus - busy )) + tests=$(( tests < 1 ? 1 : tests )) + slot_dir=$CCACHE_DIR/compile-slots + mkdir -p -m 1777 "$slot_dir" + test -w "$slot_dir" + echo "cpus=$cpus load=$busy mem_total_mb=$mem_mb -> build jobs $cpus, cold compiles $slots per host, tests $tests" + echo "CMAKE_BUILD_PARALLEL_LEVEL=$cpus" >> "$GITHUB_ENV" + echo "CTEST_PARALLEL_LEVEL=$tests" >> "$GITHUB_ENV" + echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV" + echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV" + echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV" + - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - - run: cmake --build build - - run: cmake --build build --target libs - - run: ctest --test-dir build --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml + - if: ${{ inputs.select }} + uses: actions/cache/restore@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }} + restore-keys: test-graph-${{ runner.os }}- + - name: Select the tests the change reaches + if: ${{ inputs.select }} + env: + BASE: ${{ needs.eligibility.outputs.sha }} + run: | + git diff --name-only "$BASE" HEAD > changed.txt + python3 tools/select_tests.py select build test-graph.json changed.txt + - id: build + run: | + if [ -f build/select.ninja ]; then ninja -C build -f select.ninja -j "$CMAKE_BUILD_PARALLEL_LEVEL" -l "$(nproc)" selected; else cmake --build build -- -l "$(nproc)"; fi + - id: libs + run: cmake --build build --target libs -- -l "$(nproc)" + - run: python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx - run: python3 tools/check_skips.py build/ctest.xml + - name: Write the test graph of main + if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} + continue-on-error: true + run: python3 tools/select_tests.py dump build test-graph.json + - if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }} + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ github.sha }} - name: Report compiler cache statistics if: ${{ !cancelled() }} run: ccache --show-stats @@ -372,7 +568,6 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: recursive - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -389,7 +584,28 @@ jobs: if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi done - git submodule update --init --recursive + - id: submodules + name: Key the submodules on their commits + run: | + echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT" + - id: submodule-cache + uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Check out the submodules + run: | + if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi + git submodule update --init --recursive --depth 1 --jobs 8 + - if: steps.submodule-cache.outputs.cache-hit != 'true' + run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules + - if: steps.submodule-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} - name: Point the WinLibs CMake at the CA bundle of Git for Windows run: | for prefix in ucrt64 mingw64; do @@ -409,21 +625,74 @@ jobs: - run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - uses: hendrikmuhs/ccache-action@v1.2.24 with: - key: windows - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + key: ${{ github.event_name == 'workflow_dispatch' && 'dispatch-windows' || 'windows' }} + restore-keys: ${{ github.event_name == 'workflow_dispatch' && 'windows' || '' }} + save: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} evict-old-files: job + - id: ffmpeg-cache + uses: actions/cache/restore@v6 + with: + path: build/externals + key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + - id: seed-cache + uses: actions/cache/restore@v6 + with: + path: configure-seed.cmake + key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', '.github/workflows/build.yml') }} - shell: cmd - run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") + - if: steps.seed-cache.outputs.cache-hit != 'true' + shell: cmd + run: python tools/configure_seed.py build/CMakeCache.txt configure-seed.cmake + - if: steps.seed-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: configure-seed.cmake + key: ${{ steps.seed-cache.outputs.cache-primary-key }} + - if: steps.ffmpeg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: build/externals + key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }} - name: Check that every test executable is registered with ctest shell: cmd run: python tools/check_test_registration.py build - - shell: cmd - run: cmake --build build - - shell: cmd + - if: ${{ inputs.select }} + uses: actions/cache/restore@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }} + restore-keys: test-graph-${{ runner.os }}- + - name: Select the tests the change reaches + if: ${{ inputs.select }} + env: + BASE: ${{ needs.eligibility.outputs.sha }} + shell: cmd + run: | + git diff --name-only %BASE% HEAD > changed.txt + python tools/select_tests.py select build test-graph.json changed.txt + - id: build + shell: cmd + run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build) + - id: libs + shell: cmd run: cmake --build build --target libs - shell: cmd - run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml + run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - run: python tools/check_skips.py build/ctest.xml --job windows + - name: Write the test graph of main + if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} + continue-on-error: true + shell: cmd + run: python tools/select_tests.py dump build test-graph.json + - if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }} + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ github.sha }} - name: Report tests slower than 30 s if: ${{ !cancelled() }} shell: bash @@ -431,3 +700,4 @@ jobs: [ -f ctest.log ] || exit 0 awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t } END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log + diff --git a/.github/workflows/fix-label.yml b/.github/workflows/fix-label.yml new file mode 100644 index 0000000000..978c9fb99d --- /dev/null +++ b/.github/workflows/fix-label.yml @@ -0,0 +1,44 @@ +name: Fix label + +on: + schedule: + - cron: '45 * * * *' + workflow_dispatch: + +permissions: {} + +concurrency: + group: fix-label + cancel-in-progress: true + +jobs: + label: + runs-on: ubuntu-latest + permissions: + issues: read + pull-requests: write + env: + GH_TOKEN: ${{ github.token }} + LABEL: fix error + steps: + - run: | + gh api graphql --paginate -f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f query=' + query($owner: String!, $name: String!, $endCursor: String) { + repository(owner: $owner, name: $name) { + pullRequests(states: OPEN, first: 100, after: $endCursor) { + pageInfo { hasNextPage endCursor } + nodes { number closingIssuesReferences { totalCount } labels(first: 100) { nodes { name } } } + } + } + }' --jq '.data.repository.pullRequests.nodes[] + | (.closingIssuesReferences.totalCount > 0) as $fix + | ([.labels.nodes[].name] | index(env.LABEL) != null) as $has + | if $fix and ($has | not) then "add \(.number)" elif $has and ($fix | not) then "remove \(.number)" else empty end' > "$RUNNER_TEMP/fix-todo" + label=$(jq -rn 'env.LABEL | @uri') + while read -r action number; do + if [ "$action" = add ]; then + gh api "repos/$GITHUB_REPOSITORY/issues/$number/labels" -f "labels[]=$LABEL" --silent + else + gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/$label" --silent + fi + done < "$RUNNER_TEMP/fix-todo" diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 5fa69ccd0b..ebaaec842e 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -35,4 +35,5 @@ jobs: label prx c5def5 'Other PRX libraries' label build bfd4f2 'CMake, submodules, 3rdparty' label docs 0075ca 'Documentation' + label 'fix error' d73a4a 'Closes an issue (Fixes #N)' label stale ededed 'Conflicting with main and no activity for 7 days' diff --git a/.github/workflows/stale-conflicts.yml b/.github/workflows/stale-conflicts.yml index 4eee09a40e..00150e1b70 100644 --- a/.github/workflows/stale-conflicts.yml +++ b/.github/workflows/stale-conflicts.yml @@ -14,6 +14,14 @@ jobs: issues: write pull-requests: write steps: + - env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr list -R "$GITHUB_REPOSITORY" --label stale -L 1000 --json number,labels \ + --jq '.[] | select(all(.labels[]; .name != "needs rebase")) | .number' | + while read -r number; do + gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/stale" --silent + done - uses: actions/stale@v11 with: only-pr-labels: needs rebase diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index e2b5aa9186..41602ab6fe 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -22,6 +22,12 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false + - name: Test archive root selection + shell: bash + run: | + zip_url=$(python3 -c 'from pathlib import Path; import re; print(re.search(r"from \"(https://cdn.jsdelivr.net/[^\"]+)\"", Path("demo/web/archive.mjs").read_text(encoding="utf-8")).group(1))') + curl -fsSL "$zip_url" -o "$RUNNER_TEMP/zip.mjs" + ANYPS5_TEST_ZIP_URL="$zip_url" ANYPS5_TEST_ZIP_PATH="$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())' "$RUNNER_TEMP/zip.mjs")" node demo/web/tests/archive-root.test.mjs demo/web - name: Check browser module syntax run: | node --check demo/web/app.mjs diff --git a/.gitmodules b/.gitmodules index efcc5aa70b..f68f95f507 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,9 @@ [submodule "3rdparty/mbedtls"] path = 3rdparty/mbedtls url = https://github.com/Mbed-TLS/mbedtls.git +[submodule "3rdparty/zstd"] + path = 3rdparty/zstd + url = https://github.com/facebook/zstd.git +[submodule "3rdparty/ZArchive"] + path = 3rdparty/ZArchive + url = https://github.com/Exzap/ZArchive.git diff --git a/3rdparty/ZArchive b/3rdparty/ZArchive new file mode 160000 index 0000000000..965b66c8d6 --- /dev/null +++ b/3rdparty/ZArchive @@ -0,0 +1 @@ +Subproject commit 965b66c8d67b6b7e30fd63b3b75aa91a99ff303b diff --git a/3rdparty/zstd b/3rdparty/zstd new file mode 160000 index 0000000000..f8745da6ff --- /dev/null +++ b/3rdparty/zstd @@ -0,0 +1 @@ +Subproject commit f8745da6ff1ad1e7bab384bd1f9d742439278e99 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..a1e14a99f6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# AGENTS.md + +AnyPS5 relinks PS5 executables into native Linux or Windows programs that run against reimplemented system libraries; it is not an emulator. The rules are [CONTRIBUTING.md](CONTRIBUTING.md) and [CONVENTIONS](docs/dev/CONVENTIONS.md); these are the ones agents break. + +- No comments in code, except `#endif` and namespace ends. +- An export you cannot implement calls `NotImplemented_nid_no_patch(__func__)`; never return a made-up success. +- Implement the function for every input. Never special-case a value, ID or path seen in one title's trace; if the general behaviour is unknown, throw and record it in `docs/dev/debt/.md`. +- New third-party code is a submodule at `3rdparty/`: add its `.gitmodules` entry (`git submodule add 3rdparty/`) and build it from source with CMake. Never `find_package` or copy sources in. +- Known gaps go in `docs/dev/debt/.md`; notes, reports and logs go in the pull request. Add no other files. +- Commit subjects and PR titles are Conventional Commits. +- PR body: copy [the template](.github/pull_request_template.md), keep every line, tick every box, and fill `What`, `Tested`, `Depends on` (#N or none), `Unblocks` (title ID and discussion, or none), `GPUs tested` (AGC, shader recompiler, VideoOut; otherwise n/a) and `AI-assisted` (yes or no). +- Before pushing run `python3 tools/check_conventions.py --base origin/main`; before opening run `python3 tools/pr_overlap.py --branch `. +- Build and test: [BUILD](docs/dev/BUILD.md); a relinker-only change builds with `-DANYPS5_RELINKER_ONLY=ON`. +- A speed claim shows `tools/trace_digest.py` summaries of `main` and of the change from the same machine and scene. diff --git a/CMakeLists.txt b/CMakeLists.txt index 23b603d59b..7d4eb6957c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,10 +6,21 @@ endif() project(Relinker CXX) set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp") function(add_test_executable target) add_executable(${target} ${ARGN}) set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + if(MINGW) + target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}") + target_link_options(${target} PRIVATE LINKER:--wrap=main) + endif() +endfunction() + +function(add_asan_test_options test options) + if(ANYPS5_ENABLE_ASAN) + set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}") + endif() endfunction() option(BUILD_TESTING "Build and register relinker tests" OFF) @@ -135,8 +146,12 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py) + add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py) + add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} @@ -267,6 +282,8 @@ 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) +include(cmake/ZArchive.cmake) + add_subdirectory(core/shader/recompiler) add_subdirectory(core/Decoder/Jpeg) add_subdirectory(core/Decoder/Png) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9a443afc7..1f7568da8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ For pull requests, full Linux and Windows builds and tests run only when a colla gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234 ``` -This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. +This manual run also builds and tests the relinker on Linux, Windows and macOS when the pull request changes a file the relinker-only build uses, regardless of the author's previous contributions. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`. @@ -57,7 +57,9 @@ The [pull request template](.github/pull_request_template.md) is the checklist f - 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 reported issue, write `Fixes #N` on its checklist line (`none` otherwise). GitHub closes the issue on merge, and the `fix error` label is added within the hour. - 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. +- For GPU changes, list each tested GPU's vendor, model, driver and OS on the checklist line. CI runs GPU tests only on lavapipe, so reviewers may ask for a run on another vendor before merging. - 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). diff --git a/cmake/WindowsTestExit.cpp b/cmake/WindowsTestExit.cpp new file mode 100644 index 0000000000..abe70fd2c7 --- /dev/null +++ b/cmake/WindowsTestExit.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include + +extern "C" int __real_main(int argc, char** argv); + +namespace { + +template +TFunction LibcFunction(const char* name) { + const HMODULE libc = GetModuleHandleW(L"libc.prx"); + if (libc == nullptr) return nullptr; + return reinterpret_cast(reinterpret_cast(GetProcAddress(libc, name))); +} + +} + +extern "C" int __wrap_main(int argc, char** argv) { + const int code = __real_main(argc, argv); + const auto terminate = LibcFunction("LibcTerminate_nid_no_patch"); + if (terminate == nullptr) return code; + _cexit(); + std::cout.flush(); + std::clog.flush(); + std::cerr.flush(); + std::fflush(nullptr); + terminate(code); + return code; +} diff --git a/cmake/ZArchive.cmake b/cmake/ZArchive.cmake new file mode 100644 index 0000000000..c4c7eb2024 --- /dev/null +++ b/cmake/ZArchive.cmake @@ -0,0 +1,25 @@ +enable_language(C) + +set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE) +set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE) +set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE) +add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/zstd/build/cmake ${CMAKE_BINARY_DIR}/3rdparty/zstd EXCLUDE_FROM_ALL) +set_target_properties(libzstd_static PROPERTIES POSITION_INDEPENDENT_CODE ON) + +set(ZARCHIVE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ZArchive) +add_library(zarchive STATIC EXCLUDE_FROM_ALL + ${ZARCHIVE_SOURCE_DIR}/src/zarchivereader.cpp + ${ZARCHIVE_SOURCE_DIR}/src/zarchivewriter.cpp + ${ZARCHIVE_SOURCE_DIR}/src/sha_256.c +) +target_include_directories(zarchive SYSTEM PUBLIC ${ZARCHIVE_SOURCE_DIR}/include) +target_compile_features(zarchive PUBLIC cxx_std_20) +target_link_libraries(zarchive PRIVATE libzstd_static) +set_target_properties(zarchive PROPERTIES POSITION_INDEPENDENT_CODE ON) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(zarchive INTERFACE LINKER:--exclude-libs,libzarchive.a:libzstd.a) +endif() diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8385138261..abce0455b5 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -1,5 +1,7 @@ if(NOT WIN32) - string(REPLACE "-static " "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ") + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() string(STRIP "${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS) endif() @@ -221,6 +223,8 @@ if(BUILD_TESTING) 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_elf_duplicates COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_duplicates.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} @@ -238,6 +242,17 @@ if(BUILD_TESTING) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) endif() add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR}) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(host_libstdcxx_exception_module SHARED tests/HostLibstdcxxExceptionModule.cpp) + target_link_libraries(host_libstdcxx_exception_module PRIVATE ${PATCHED_DIR}/libc.prx) + add_dependencies(host_libstdcxx_exception_module libs) + add_test_executable(host_libstdcxx_exception_tests tests/HostLibstdcxxException.cpp) + target_link_libraries(host_libstdcxx_exception_tests PRIVATE host_libstdcxx_exception_module ${PATCHED_DIR}/libc.prx) + add_dependencies(host_libstdcxx_exception_tests libs) + set_target_properties(host_libstdcxx_exception_module host_libstdcxx_exception_tests PROPERTIES BUILD_RPATH ${PATCHED_DIR}) + add_test(NAME host_libstdcxx_exception COMMAND host_libstdcxx_exception_tests) + set_tests_properties(host_libstdcxx_exception PROPERTIES TIMEOUT 20) + endif() if(Python3_Interpreter_FOUND) add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $) @@ -277,6 +292,35 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") endif() + add_test_executable(crash_text_tests tests/CrashText.cpp) + target_include_directories(crash_text_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(MINGW) + target_compile_options(crash_text_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME crash_text COMMAND crash_text_tests) + + if(WIN32) + add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp) + add_dependencies(windows_crash_text_tests libc) + if(MINGW) + target_compile_options(windows_crash_text_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_crash_text COMMAND windows_crash_text_tests $) + set_tests_properties(windows_crash_text PROPERTIES TIMEOUT 20 + PASS_REGULAR_EXPRESSION "faulting[^~]*FATAL: unhandled exception 0xc0000005[^~]* \\[r12\\] \"allocation failed in the heap\"[\r\n]+ \\[r13\\] u\"LowLevelFatalError \\[File:Unknown\\] \\[Line: 42\\] Out of memory\"[\r\n]+ \\[r14\\] \"PAGE END\"[.][.][.]" + FAIL_REGULAR_EXPRESSION "\\[rbx\\] u?\"|did not fault") + endif() + + add_test_executable(guest_voice_port_attr_tests tests/GuestVoicePortAttr.cpp) + target_include_directories(guest_voice_port_attr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_voice_port_attr_tests PRIVATE libSceVoice libc) + configure_windows_unwind(guest_voice_port_attr_tests) + add_test(NAME guest_voice_port_attr COMMAND guest_voice_port_attr_tests) + if(WIN32) + set_tests_properties(guest_voice_port_attr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) @@ -351,6 +395,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rudp_context_lifecycle_tests tests/GuestRudpContextLifecycle.cpp) + target_include_directories(guest_rudp_context_lifecycle_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_context_lifecycle_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_context_lifecycle_tests) + add_test(NAME guest_rudp_context_lifecycle COMMAND guest_rudp_context_lifecycle_tests) + if(WIN32) + set_tests_properties(guest_rudp_context_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) @@ -458,16 +512,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - add_test_executable(guest_ppsa01530_exports_tests tests/GuestPPSA01530Exports.cpp) - target_include_directories(guest_ppsa01530_exports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_ppsa01530_exports_tests PRIVATE libkernel libSceJson2 libSceNet libScePad libc) - configure_windows_unwind(guest_ppsa01530_exports_tests) - add_test(NAME guest_ppsa01530_exports COMMAND guest_ppsa01530_exports_tests) - if(WIN32) - set_tests_properties(guest_ppsa01530_exports PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() - add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp) target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 1e46d81df2..5d444a5034 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -160,18 +160,23 @@ struct VirtualQueryInfo { std::uint64_t offset; std::int32_t protection; std::int32_t memory_type; - std::uint32_t is_flexible : 1; - std::uint32_t is_direct : 1; - std::uint32_t is_stack : 1; - std::uint32_t is_pooled : 1; - std::uint32_t is_committed : 1; - std::uint32_t is_gpu_prt : 1; - std::uint32_t amm_usage : 1; - std::uint32_t reserved : 1; + std::uint8_t is_flexible : 1; + std::uint8_t is_direct : 1; + std::uint8_t is_stack : 1; + std::uint8_t is_pooled : 1; + std::uint8_t is_committed : 1; + std::uint8_t is_gpu_prt : 1; + std::uint8_t amm_usage : 1; + std::uint8_t reserved : 1; char name[32]; std::uint8_t gpu_mask_id; std::uint8_t reserved2; }; +static_assert(sizeof(VirtualQueryInfo) == 72); +static_assert(alignof(VirtualQueryInfo) == 8); +static_assert(offsetof(VirtualQueryInfo, name) == 33); +static_assert(offsetof(VirtualQueryInfo, gpu_mask_id) == 65); +static_assert(offsetof(VirtualQueryInfo, reserved2) == 66); struct KernelSemaPrivate; struct KernelEventFlagPrivate; @@ -675,10 +680,13 @@ struct AvPlayerVideoEx { std::uint8_t luma_bit_depth; std::uint8_t chroma_bit_depth; bool video_full_range_flag; - std::uint8_t reserved1[37]; + std::uint8_t reserved1[5]; + double frame_rate; + std::uint8_t reserved2[24]; }; static_assert(sizeof(AvPlayerVideoEx) == 80); static_assert(offsetof(AvPlayerVideoEx, crop_left_offset) == 20 && offsetof(AvPlayerVideoEx, pitch) == 36); +static_assert(offsetof(AvPlayerVideoEx, frame_rate) == 48); struct AvPlayerTimedTextEx { std::uint8_t language_code[4]; @@ -913,6 +921,14 @@ struct VoicePortInfo { std::uint16_t reserved; }; +struct VoiceResourceInfo { + std::uint16_t max_voice_input_ports; + std::uint16_t max_voice_output_ports; + std::uint16_t max_device_input_ports; + std::uint16_t max_device_output_ports; + std::uint16_t max_ports; +}; + struct VoiceStartParam { void* container; std::uint32_t mem_size; @@ -1960,6 +1976,42 @@ struct VideoOutVblankStatus { std::uint8_t pad1[6] = {}; }; +struct VideoOutBufferAttribute { + std::uint32_t pixel_format; + std::uint32_t tiling_mode; + std::uint32_t aspect_ratio; + std::uint32_t width; + std::uint32_t height; + std::uint32_t pitch_in_pixel; + std::uint32_t option; + std::uint32_t reserved0; + std::uint64_t reserved1; +}; +static_assert(sizeof(VideoOutBufferAttribute) == 40); + +struct VideoOutFlipStatusPs4 { + std::uint64_t count = 0; + std::uint64_t processTime = 0; + std::uint64_t tsc = 0; + std::int64_t flipArg = 0; + std::uint64_t submitTsc = 0; + std::uint64_t reserved0 = 0; + std::int32_t gcQueueNum = 0; + std::int32_t flipPendingNum = 0; + std::int32_t currentBuffer = 0; + std::uint32_t reserved1 = 0; +}; +static_assert(sizeof(VideoOutFlipStatusPs4) == 64); + +struct VideoOutVblankStatusPs4 { + std::uint64_t count = 0; + std::uint64_t processTime = 0; + std::uint64_t tsc = 0; + std::uint8_t flags = 0; + std::uint8_t pad[7] = {}; +}; +static_assert(sizeof(VideoOutVblankStatusPs4) == 32); + struct VideoOutOutputStatus { std::uint32_t resolution = 0; std::uint32_t dynamicRange = 0; @@ -1988,6 +2040,11 @@ struct VideoOutColorSettings { std::uint32_t reserved[3] = {}; }; +struct VideoOutColorSpaceConversionSettings { + std::uint32_t internalData[8] = {}; +}; +static_assert(sizeof(VideoOutColorSpaceConversionSettings) == 32); + using atexit_func_t = void (*)(); diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index b5470147d9..7a6b8c3d1e 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -408,6 +409,7 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& }; std::vector uses; + std::unordered_map> patchedExports; 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); @@ -424,6 +426,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const auto it = nidMap.find(symName); if (it != nidMap.end()) newValue = it->second; + const auto [existing, inserted] = patchedExports.try_emplace(newValue, sym.st_value, sym.st_shndx); + if (!inserted && existing->second != std::pair{sym.st_value, sym.st_shndx}) + throw std::runtime_error("duplicate patched export name: " + newValue); } else if (binding != kStbLocal && sym.st_shndx == kShnUndef) { const bool isUnversioned = versymOffset == 0u || (Read(elf, versymOffset + i * sizeof(std::uint16_t)) <= 1u); diff --git a/core/libs/nid/tests/test_elf_duplicates.py b/core/libs/nid/tests/test_elf_duplicates.py new file mode 100644 index 0000000000..504cf33bdc --- /dev/null +++ b/core/libs/nid/tests/test_elf_duplicates.py @@ -0,0 +1,69 @@ +import base64 +import hashlib +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC + + +def nid(name): + digest = hashlib.sha1(name.encode() + bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")).digest() + return base64.b64encode(digest[:8][::-1]).decode().rstrip("=").replace("/", "-") + + +def image(names, addresses=None): + if addresses is None: + addresses = [0x1200 + index * 0x20 for index in range(len(names))] + entries = [(None, 1, SHN_UNDEF, 0, 0)] + entries += [(name, 1, SHN_UNDEF, 0, 0) for name in IMPORTS] + entries += [(name, 1, 9, STT_FUNC, address) for name, address in zip(names, addresses)] + return bytes(build_image(entries, False)) + + +class ElfDuplicateTests(unittest.TestCase): + def test_exports_patched_to_one_name_are_rejected(self): + cases = ( + ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_disambig1", "sceBetaCopyString"], + ["sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_disambig12", "sceBetaCopyString"], + ["sceBetaCopyString", "sceAlphaComputeLength_nid_no_patch", "sceAlphaComputeLength_nid_no_patch_nid_no_patch_cut"], + ) + for names in cases: + with self.subTest(names=names): + before = image(names) + result, after = patch(PATCHER, before, "duplicates.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertIn(b"duplicate patched export name", result.stderr) + self.assertEqual(after, before) + + def test_aliases_of_one_address_are_patched(self): + names = ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_no_patch_cut", "sceBetaCopyString"] + result, after = patch(PATCHER, image(names, [0x1200, 0x1200, 0x1200, 0x1220]), "aliases.so") + self.assertEqual(result.returncode, 0, result.stderr) + patched = [(entry[0], entry[2]) for entry in read_dynsym(after) if entry[1] == 9] + self.assertEqual(sorted(name for name, address in patched if address == 0x1200), + sorted(["sceAlphaComputeLength", "sceAlphaComputeLength", nid("sceAlphaComputeLength")])) + self.assertEqual([name for name, address in patched if address == 0x1220], [nid("sceBetaCopyString")]) + self.assertTrue(NID_NAME.match(nid("sceAlphaComputeLength"))) + + def test_aliases_of_different_addresses_are_rejected(self): + names = ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_no_patch_cut"] + before = image(names, [0x1200, 0x1200, 0x1220]) + result, after = patch(PATCHER, before, "aliases.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertIn(b"duplicate patched export name: sceAlphaComputeLength", result.stderr) + self.assertEqual(after, before) + + def test_distinct_exports_are_still_patched(self): + names = ["sceAlphaComputeLength_nid_disambig1", "sceBetaCopyString_nid_postfix", "sceGammaKeep_nid_no_patch_cut"] + result, after = patch(PATCHER, image(names), "distinct.so") + self.assertEqual(result.returncode, 0, result.stderr) + patched = [entry[0] for entry in read_dynsym(after) if entry[1] == 9] + self.assertEqual(len(patched), 3) + self.assertEqual(len(set(patched)), 3) + self.assertIn("sceGammaKeep", patched) + + +if __name__ == "__main__": + PATCHER = Path(sys.argv.pop(1)).resolve() + unittest.main() diff --git a/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp b/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp index a072cf5f5c..abd502cc9d 100644 --- a/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp +++ b/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp @@ -6,6 +6,7 @@ namespace Agc::Command { void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function); +int GetDefaultCxStateFlat(void* destination, std::uint32_t size); } diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 4bf5a844f4..743fed53f2 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -107,7 +107,10 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s } std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const volatile ShaderRegister* registers, std::uint32_t count, bool snapshotAll, const char* function) { - Require(count != 0, function, "empty register list"); + if (count == 0) { + Require(buffer != nullptr, function, "null command buffer"); + return buffer->cursor_up; + } CheckAddress(reinterpret_cast(registers), 4, function); std::vector snapshot; if (snapshotAll) { diff --git a/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp index ca1d966c3f..189b8fb9fa 100644 --- a/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,33 @@ struct Storage { std::mutex defaultsMutex; std::array, 2> storage; +struct AgcContextState { + std::uint32_t data[512]; +}; + +static_assert(sizeof(AgcContextState) == 2048); + +std::mutex defaultStateMutex; +AgcContextState defaultState{}; +bool defaultStateInitialized = false; + +void InitDefaultCxState() { + std::memset(&defaultState, 0, sizeof(defaultState)); + const auto* source = agcPublicRegDefaultsByVersion[8]; + for (std::size_t bank = 0; bank < 4; ++bank) { + const auto& b = source->banks[bank]; + if (b.registers == nullptr) continue; + for (std::uint32_t r = 0; r < b.registerCount; ++r) { + const auto offset = b.registers[r].offset; + const auto value = b.registers[r].value; + if (offset < 512) { + defaultState.data[offset] = value; + } + } + } + defaultStateInitialized = true; +} + } void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function) { @@ -76,4 +104,15 @@ void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* func return &destination.defaults; } +int GetDefaultCxStateFlat(void* destination, std::uint32_t size) { + if (destination == nullptr) return static_cast(0x80890001); + std::lock_guard lock(defaultStateMutex); + if (!defaultStateInitialized) { + InitDefaultCxState(); + } + const auto copyBytes = std::min(size, static_cast(sizeof(AgcContextState))); + std::memcpy(destination, defaultState.data, copyBytes); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp b/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp index cb296a8167..c7eb4fea76 100644 --- a/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp +++ b/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp @@ -1,4 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_REGISTERDEFAULTS_HPP #define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_REGISTERDEFAULTS_HPP +#include "SceTypes.hpp" +#include +extern "C" { +int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size); +} #endif diff --git a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp index d5ff3372f1..487bc16fca 100644 --- a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp @@ -26,4 +26,8 @@ void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version) { return Agc::Command::GetRegisterDefaults(version, true, __func__); } +int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size) { + return Agc::Command::GetDefaultCxStateFlat(out_state, size); +} + } diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 1f3c3ac3c6..8e56d4c0c8 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -127,13 +127,11 @@ int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Sh return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } -APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); -int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { +int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); } -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) { +int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index dad824f489..7a7d7c52cb 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -30,11 +30,6 @@ int APS5_VABI sceAgcDcbSetMarkerSpan() { return 0; } -int APS5_VABI sceAgcGetDefaultCxStateFlat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcSetSemaphoreMemory() { 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 7c90e82644..1f86645fe8 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -21,6 +21,7 @@ extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal(); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version); +extern "C" int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size); extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier); @@ -637,6 +638,21 @@ void testDefaults() { auto* internalDefaults = sceAgcGetRegisterDefaultsInternal(); check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table"); check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table"); + constexpr int kInvalidArgument = static_cast(0x80890001u); + std::array flatCxBuffer{}; + check(sceAgcGetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null destination was accepted"); + check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), 0) == 0, "zero size copy failed"); + check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "flat CX state default retrieval failed"); + check(flatCxBuffer[0x001] == 0x11000100u, "flat CX state register 0x001 mismatch"); + check(flatCxBuffer[0x007] == 0x0000ffffu, "flat CX state register 0x007 mismatch"); + check(flatCxBuffer[0x12d] == 0x3f800000u, "flat CX state register 0x12d mismatch"); + check(flatCxBuffer[0x1e0] == 0x20010001u, "flat CX state register 0x1e0 mismatch"); + std::array overBuffer{}; + overBuffer[512] = 0x12345678u; + check(sceAgcGetDefaultCxStateFlat(overBuffer.data(), sizeof(overBuffer)) == 0, "oversized copy failed"); + check(overBuffer[512] == 0x12345678u, "oversized copy overflowed buffer"); + check(Agc::Command::GetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null command destination was accepted"); + check(Agc::Command::GetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "command flat CX state retrieval failed"); expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); }); expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); } diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index b176809a48..652eacfea7 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -12,8 +12,8 @@ #include extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); -extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps); -extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { @@ -61,14 +61,14 @@ void checkIdentity(const Registers& regs, std::uint32_t first, const char* messa void testIdentity() { auto regs = filled(); - check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed"); + check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed"); checkIdentity(regs, 0, "mapping without a pixel shader is not the identity"); Shader ps{}; regs = filled(); - check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); + check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); checkIdentity(regs, 0, "mapping without inputs is not the identity"); regs = filled(); - check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); + check(sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); check(preparedCount == 0u, "update without inputs prepared registers"); const auto untouched = filled(); check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers"); @@ -94,7 +94,7 @@ void testMapping() { ps.num_input_semantics = static_cast(inputs.size()); auto split = filled(); - check(sceAgcUnknownCreateInterpolantMapping(split.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(sceAgcCreateInterpolantMapping_10000(split.data(), &gs, &ps) == 0, "split f16 mapping failed"); check(std::memcmp(split.data(), preparedMapping.data(), sizeof(split)) == 0, "prepared split mapping differs from published registers"); for (std::uint32_t i = 0; i < inputs.size(); ++i) { check(split[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "interpolant register offset changed"); @@ -109,7 +109,7 @@ void testMapping() { auto update = filled(); const auto untouched = filled(); const std::size_t written = inputs.size() * sizeof(ShaderRegister); - check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed"); check(preparedCount == inputs.size(), "update prepared registers past its inputs"); check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers"); check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping"); @@ -142,7 +142,7 @@ void testUpdateMapping() { auto update = filled(); const auto untouched = filled(); - check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed"); check(update[0].value == 0x00000625u, "custom interpolant update changed"); check(update[1].value == 0x01380107u, "low f16 half update changed"); check(update[2].value == 0x02480220u, "unmatched high f16 half update changed"); @@ -154,14 +154,14 @@ void testUpdateMapping() { check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); auto create = filled(); - check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(sceAgcCreateInterpolantMapping_10000(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); for (const std::uint32_t index : {1u, 3u}) { inputs[index].default_value = 0; update = filled(); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); }); check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); inputs[index].default_value = inputs[index].default_value_hi; } @@ -177,15 +177,15 @@ void testRejections() { std::memset(regs.data(), 0xcc, sizeof(regs)); const auto saved = regs; expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); }); - expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, &ps); }); check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers"); ps.num_input_semantics = 1; - expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); }); - expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); }); + expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcCreateInterpolantMapping_10000(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, nullptr); }); } } diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index a4ac992501..a1e0295daf 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -193,6 +193,10 @@ void testDirectList() { packet = writers[i](&split.buffer, paired.data(), 4); check(std::equal(expectedSplit.begin(), expectedSplit.end(), packet) && split.buffer.cursor_up == packet + expectedSplit.size(), "register pairs were not written as two ranges"); check(sizes[i](0) == 0, "an empty register list has a size"); + Storage empty; + empty.words.fill(sentinel); + check(writers[i](&empty.buffer, nullptr, 0) == empty.words.data() && empty.buffer.cursor_up == empty.words.data() && empty.words.front() == sentinel, "an empty register list wrote a packet"); + expectFailure([&] { writers[i](nullptr, nullptr, 0); }); check(sizes[i](0x15555555u) == 0xfffffffcu, "largest register list size mismatch"); for (const auto count : {0x15555556u, 0x40000000u, 0xffffffffu}) { expectFailure([&] { sizes[i](count); }); diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index ad88c7a410..46f7c3c8e8 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgc/Shader/include/PrimState.hpp" #include "prx/libSceAgc/Shader/include/InterpolantMapping.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -194,9 +195,11 @@ int main() { Require(preparations == 0 && mappings == 0 && links == 0); OptionalGeometryOutputRegister(special, vertex, hull); HandBuiltUserVgprRegister(); + LibcRunShutdown_nid_postfix(); std::cout << "shader helper preparation tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} return 1; } } diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5..fd05a08b8e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -154,6 +154,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPrecheck.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRegisterKey.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRelocate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawTiming.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/IndirectDraw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/EntryPoints.cpp @@ -173,6 +174,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Presentation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/QueueWorker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/Submission.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/SubmissionRing.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/WorkerAffinity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -204,6 +206,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp @@ -316,6 +319,17 @@ set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceAgcDriver) if(BUILD_TESTING) + add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp) + target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_frame_dump_io_tests PRIVATE libc) + configure_windows_unwind(agc_frame_dump_io_tests) + add_test(NAME agc_frame_dump_io COMMAND agc_frame_dump_io_tests) + set_tests_properties(agc_frame_dump_io PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_frame_dump_io PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_write_watch_coverage_tests tests/WriteWatchCoverage.cpp) target_include_directories(agc_write_watch_coverage_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_write_watch_coverage_tests PRIVATE libc) @@ -337,6 +351,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(agc_async_log_tests tests/AsyncLog.cpp) + target_include_directories(agc_async_log_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_async_log_tests PRIVATE libc) + configure_windows_unwind(agc_async_log_tests) + add_test(NAME agc_async_log COMMAND agc_async_log_tests) + add_test(NAME agc_async_log_fatal COMMAND agc_async_log_tests fatal) + set_tests_properties(agc_async_log agc_async_log_fatal PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_async_log agc_async_log_fatal 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) @@ -358,6 +384,17 @@ if(BUILD_TESTING) set_tests_properties(agc_performance_trace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(agc_handoff_spin_tests tests/HandoffSpin.cpp) + target_include_directories(agc_handoff_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_handoff_spin_tests PRIVATE libc) + configure_windows_unwind(agc_handoff_spin_tests) + add_test(NAME agc_handoff_spin COMMAND agc_handoff_spin_tests) + set_tests_properties(agc_handoff_spin PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_handoff_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) @@ -750,6 +787,24 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_pipeline_queue_failure_tests EXCLUDE_FROM_ALL tests/PipelineQueueFailure.cpp + Graphics/src/PipelineLibrary.cpp tests/AllocationFailure.cpp) +target_include_directories(agc_pipeline_queue_failure_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_pipeline_queue_failure_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_pipeline_queue_failure_tests) +if(BUILD_TESTING) + set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(mode wait clear stop) + add_test(NAME agc_pipeline_queue_failure_${mode} COMMAND agc_pipeline_queue_failure_tests ${mode}) + set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() +endif() + add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp) target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_pass_hazards_tests PRIVATE libc) @@ -771,6 +826,7 @@ if(BUILD_TESTING) endif() add_test_executable(agc_buffer_pool_tests EXCLUDE_FROM_ALL tests/BufferPool.cpp) +target_sources(agc_buffer_pool_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp) target_include_directories(agc_buffer_pool_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_buffer_pool_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_buffer_pool_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -778,8 +834,14 @@ configure_windows_unwind(agc_buffer_pool_tests) if(BUILD_TESTING) set_target_properties(agc_buffer_pool_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_buffer_pool COMMAND agc_buffer_pool_tests) + add_test(NAME agc_buffer_pool_small_device_shared COMMAND agc_buffer_pool_tests --small-device-retention) + set_tests_properties(agc_buffer_pool_small_device_shared PROPERTIES ENVIRONMENT + "APS5_STAGING_POOL_MIB=256;APS5_BUFFER_POOL_SHARED=1") + add_test(NAME agc_buffer_pool_small_device_no_staging COMMAND agc_buffer_pool_tests --small-device-retention) + set_tests_properties(agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT + "APS5_STAGING_POOL_MIB=0") if(WIN32) - set_tests_properties(agc_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_buffer_pool agc_buffer_pool_small_device_shared agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -1029,6 +1091,11 @@ set_target_properties(agc_driver_recorder_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_recorder_tests) if(BUILD_TESTING) set_target_properties(agc_driver_recorder_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_recorder_device_selection COMMAND agc_driver_recorder_tests --device-selection-only) + if(WIN32) + set_tests_properties(agc_recorder_device_selection PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) @@ -1074,6 +1141,12 @@ if(BUILD_TESTING) "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_sampled_storage_base_level_usage COMMAND agc_driver_recorder_tests --sampled-storage-base-level-usage-only) + set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() 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) if(WIN32) @@ -1186,6 +1259,23 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_command_batch_failure_tests EXCLUDE_FROM_ALL + tests/CommandBatch.cpp tests/CommandBatchFailure.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp) +target_include_directories(agc_command_batch_failure_tests PRIVATE + ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_command_batch_failure_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_command_batch_failure_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_command_batch_failure_tests) +if(BUILD_TESTING) + set_target_properties(agc_command_batch_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_command_batch_failure COMMAND agc_command_batch_failure_tests) + set_tests_properties(agc_command_batch_failure PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_command_batch_failure PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp) target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc) @@ -1275,6 +1365,22 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/DepthSurfaceReuse.cpp ) +if(BUILD_TESTING) + add_test_executable(agc_descriptor_cache_failure_tests + tests/DescriptorCacheFailure.cpp + Graphics/src/ShaderResources.cpp) + target_include_directories(agc_descriptor_cache_failure_tests PRIVATE + $) + target_link_libraries(agc_descriptor_cache_failure_tests PRIVATE libSceAgcDriver shader_recompiler_agc_driver libc libkernel) + configure_windows_unwind(agc_descriptor_cache_failure_tests) + add_test(NAME agc_descriptor_cache_failure COMMAND agc_descriptor_cache_failure_tests) + set_tests_properties(agc_descriptor_cache_failure PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_descriptor_cache_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(agc_driver_graphics_tests PRIVATE ${LIBS_INCLUDE_DIR} $ @@ -1375,14 +1481,29 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) configure_windows_unwind(${agcExecutionTest}) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +if(BUILD_TESTING) + add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets) + set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() 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() +if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXECUTION_TESTS) + target_sources(agc_driver_pooled_buffer_range_tests PRIVATE + Graphics/src/BufferPool.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp tests/CommandBatchFailure.cpp) +endif() + target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) + target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) @@ -1390,19 +1511,46 @@ target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) if(BUILD_TESTING) + add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity) + set_tests_properties(agc_driver_store_pending_identity PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input) + set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered8) + set(multisampledDccClearTests) + foreach(samples IN ITEMS 2 4 8) + set(testName "agc_driver_msaa_dcc_clear_${samples}") + add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples}) + list(APPEND multisampledDccClearTests ${testName}) + endforeach() + set_tests_properties(agc_driver_msaa_dcc_state ${multisampledDccClearTests} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(${multisampledDccClearTests} PROPERTIES SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -1418,6 +1566,8 @@ target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $< target_include_directories(agc_driver_color1555_target_tests PRIVATE $) +target_include_directories(agc_driver_color565_target_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) @@ -1438,8 +1588,12 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $) + target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) +target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) + target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $) @@ -1498,8 +1652,10 @@ if(BUILD_TESTING) add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget) + set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests) 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") @@ -1519,6 +1675,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file) + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) + if(WIN32) + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() if(WIN32) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 @@ -1536,6 +1697,15 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) + add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) + add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) + add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) + set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -1657,13 +1827,31 @@ if(BUILD_TESTING) 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 TIMEOUT 30) + set(pm4RingCases "ring|APS5_PM4_RING=4" "ring-empty|APS5_PM4_RING=16" "ring-disabled|APS5_PM4_RING=0" "ring-invalid|APS5_PM4_RING=4x" "ring-report|APS5_PM4_RING=2" "stall-watchdog|APS5_STALL_WATCHDOG_SECS=1" + "stall-watchdog-invalid|APS5_STALL_WATCHDOG_SECS=1x" "stall-watchdog-rewind|APS5_STALL_WATCHDOG_SECS=1" "stall-watchdog-order|APS5_STALL_WATCHDOG_SECS=1") + if(WIN32) + list(APPEND pm4RingCases "ring-report-heap|APS5_PM4_RING=2") + endif() + foreach(pm4RingCase IN LISTS pm4RingCases) + string(REPLACE "|" ";" pm4RingCase "${pm4RingCase}") + list(GET pm4RingCase 0 pm4RingMode) + list(GET pm4RingCase 1 pm4RingEnvironment) + string(REPLACE "-" "_" pm4RingTest "agc_driver_pm4_${pm4RingMode}") + add_test(NAME ${pm4RingTest} COMMAND agc_driver_pm4_tests ${pm4RingMode}) + set_tests_properties(${pm4RingTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;${pm4RingEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(${pm4RingTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure) + add_test(NAME agc_descriptor_cache COMMAND agc_driver_graphics_tests --descriptor-cache) add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown) set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_descriptor_cache agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(NOT WIN32) @@ -1699,6 +1887,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_stencil_replay_no_gpl PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_pipelined_dispatch_item COMMAND agc_driver_pipelined_dispatch_tests) + set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINE_DISPATCH=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) if(NOT TARGET ${agcTest}) @@ -1730,7 +1924,7 @@ if(BUILD_TESTING) endif() endforeach() endforeach() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync) + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync lazy lazy_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode STREQUAL "indirect") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect) @@ -1738,6 +1932,8 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred) elseif(preparedMode STREQUAL "deferred_undecodable") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable) + elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync") + add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --lazy) elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup) elseif(preparedMode STREQUAL "debug_failure") @@ -1751,6 +1947,11 @@ if(BUILD_TESTING) set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1") elseif(preparedMode STREQUAL "registered_workgroup_sync") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") + elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_LAZY_SHADER_PREPARE=1" "APS5_SUSPEND_DRAIN=1") + if(preparedMode STREQUAL "lazy_sync") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") + endif() elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1") endif() @@ -1758,6 +1959,17 @@ if(BUILD_TESTING) set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) + add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) + add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) + add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) + add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) + add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held) + add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) @@ -1872,6 +2084,25 @@ endif() set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_replay) +if(BUILD_TESTING) + add_test_executable(agc_shader_replay_requests_tests tests/ShaderReplayRequests.cpp) + target_link_libraries(agc_shader_replay_requests_tests PRIVATE shader_recompiler_agc_driver libc) + set_target_properties(agc_shader_replay_requests_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_shader_replay_requests_tests) + add_dependencies(agc_shader_replay_requests_tests agc_shader_replay) + set(agcShaderReplayCapture "${CMAKE_CURRENT_BINARY_DIR}/shader-replay-capture") + add_test(NAME agc_shader_replay_requests COMMAND agc_shader_replay_requests_tests "${agcShaderReplayCapture}") + set_tests_properties(agc_shader_replay_requests PROPERTIES FIXTURES_SETUP agc_shader_replay_capture TIMEOUT 30) + add_test(NAME agc_shader_replay_tool COMMAND ${CMAKE_COMMAND} -DTOOL=$ "-DCAPTURE=${agcShaderReplayCapture}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderReplayTool.cmake) + set_tests_properties(agc_shader_replay_tool PROPERTIES FIXTURES_REQUIRED agc_shader_replay_capture TIMEOUT 30) + set_tests_properties(agc_shader_replay_requests agc_shader_replay_tool PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_shader_replay_requests agc_shader_replay_tool PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_executable(agc_pm4_replay EXCLUDE_FROM_ALL tools/Pm4Replay.cpp tools/Pm4Analysis.cpp @@ -2345,3 +2576,9 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + get_directory_property(agcRegisteredTests TESTS) + list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$") + set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") +endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp new file mode 100644 index 0000000000..36b218edf9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -0,0 +1,418 @@ +#pragma once + +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/LogMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct LogLiteral { + const char* text; +}; + +namespace AsyncLogDetail { + +template +inline constexpr bool Deferrable = std::is_arithmetic_v || std::is_enum_v || std::is_same_v; + +inline const char* Unwrap(LogLiteral value) { + return value.text; +} + +template +T Unwrap(T value) { + return value; +} + +class Formatter { +public: + virtual ~Formatter() = default; + virtual void Format(std::string& out) const = 0; +}; + +template +class Bound final : public Formatter { +public: + Bound(const char* format, TArgs... args) : format(format), values(args...) {} + + void Format(std::string& out) const override { + std::apply([&](const auto&... value) { Append(out, value...); }, values); + } + +private: + void Append(std::string& out, const auto&... value) const { + char small[512]; + const int size = std::snprintf(small, sizeof(small), format, Unwrap(value)...); + if (size < 0) return; + if (static_cast(size) < sizeof(small)) { + out.append(small, static_cast(size)); + return; + } + std::string large(static_cast(size) + 1, '\0'); + std::snprintf(large.data(), large.size(), format, Unwrap(value)...); + out.append(large.data(), static_cast(size)); + } + + const char* format; + std::tuple values; +}; + +struct Entry { + std::FILE* stream = nullptr; + long long micros = -1; + std::string text; + std::unique_ptr deferred; + std::size_t cost = 0; +}; + +} + +class AsyncLog { +public: + using Clock = std::chrono::steady_clock; + + static constexpr std::size_t Capacity = 16u * 1024u * 1024u; + + static AsyncLog& Get() { + static AsyncLog* const instance = new AsyncLog(); + return *instance; + } + + int Write(std::FILE* stream, const char* text, std::size_t length) { + const auto start = Clock::now(); + if (!asynchronous) return WriteNow(stream, text, length); + AsyncLogDetail::Entry entry; + entry.stream = stream; + entry.text.assign(text, length); + entry.cost = sizeof(entry) + length; + Enqueue(std::move(entry)); + callerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); + return 0; + } + + template + void Defer(std::FILE* stream, const char* format, TArgs... args) { + static_assert((AsyncLogDetail::Deferrable && ...), "deferred log arguments are numbers, enums or LogLiteral"); + const auto start = Clock::now(); + const auto micros = LogTimestampMicros(); + auto bound = std::make_unique>(format, args...); + if (!asynchronous) { + std::string text; + if (micros >= 0) AppendTimestamp(text, micros); + bound->Format(text); + WriteNow(stream, text.data(), text.size()); + return; + } + AsyncLogDetail::Entry entry; + entry.stream = stream; + entry.micros = micros; + entry.deferred = std::move(bound); + entry.cost = sizeof(entry) + sizeof(AsyncLogDetail::Bound); + Enqueue(std::move(entry)); + callerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); + } + + bool Admit(const std::string& line) { + const auto now = Clock::now(); + std::string summary; + { + std::lock_guard lock(coalesceMutex); + auto found = windows.find(line); + if (found == windows.end()) { + if (windows.size() >= CoalesceKeys) return true; + windows.emplace(line, Window{now, 1, 0}); + return true; + } + auto& window = found->second; + if (now - window.first < CoalesceWindow) { + if (++window.seen <= PrintedRepeats) return true; + ++window.suppressed; + coalescedTotal.fetch_add(1, std::memory_order_relaxed); + return false; + } + summary = Summary(found->first, window.suppressed); + window = Window{now, 1, 0}; + } + if (!summary.empty()) WriteNow(stderr, summary.data(), summary.size()); + return true; + } + + void Flush() { + if (!asynchronous) return; + std::unique_lock lock(mutex); + const auto target = queued.load(std::memory_order_relaxed); + idle.wait(lock, [&] { return finished.load(std::memory_order_relaxed) >= target; }); + } + + bool FlushFor(std::chrono::milliseconds limit) { + if (!asynchronous) return true; + const auto target = queued.load(std::memory_order_acquire); + const auto deadline = Clock::now() + limit; + while (finished.load(std::memory_order_acquire) < target) { + if (Clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; + } + + static void FlushAtEnd() { + auto& log = Get(); + if (log.abandoned.load(std::memory_order_acquire)) return; + if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release); + } + + std::unique_lock Hold() { + return std::unique_lock(mutex); + } + +private: + struct Window { + Clock::time_point first; + std::uint64_t seen; + std::uint64_t suppressed; + }; + + static constexpr std::size_t CoalesceKeys = 128; + static constexpr std::uint64_t PrintedRepeats = 3; + static constexpr std::chrono::seconds CoalesceWindow{10}; + static constexpr std::chrono::seconds StatsWindow{10}; + static constexpr std::chrono::milliseconds Tick{500}; + static constexpr std::chrono::milliseconds EndFlushLimit{2000}; + + AsyncLog() { + asynchronous = std::getenv("APS5_LOG_SYNC") == nullptr; + const char* stats = std::getenv("APS5_LOG_STATS"); + statsEnabled = stats == nullptr || std::string(stats) != "0"; + lastStats = Clock::now(); + if (!asynchronous) return; + LogSink().store(&AsyncLog::Sink, std::memory_order_release); + std::atexit(&AsyncLog::FlushAtEnd); + InitializeWaits(); + std::thread([this] { Run(); }).detach(); + LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd); + } + + void InitializeWaits() { + std::unique_lock lock(mutex); + for (auto* condition : {&wake, &space, &idle}) static_cast(condition->wait_for(lock, std::chrono::nanoseconds(0))); + } + + static int Sink(std::FILE* stream, const char* text, std::size_t length) { + return Get().Write(stream, text, length); + } + + static std::uint64_t Elapsed(Clock::time_point start) { + return static_cast(std::chrono::duration_cast(Clock::now() - start).count()); + } + + static void AppendTimestamp(std::string& out, long long micros) { + char prefix[64]; + const int size = FormatLogTimestamp(prefix, sizeof(prefix), micros); + if (size > 0) out.append(prefix, static_cast(size)); + } + + static std::string Summary(const std::string& line, std::uint64_t suppressed) { + if (suppressed == 0) return {}; + std::string text = line; + while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) text.pop_back(); + text += " (repeated " + std::to_string(suppressed) + " more times in the last 10 s)\n"; + return text; + } + + static int WriteNow(std::FILE* stream, const char* text, std::size_t length) { + std::fwrite(text, 1, length, stream); + return std::fflush(stream); + } + + void Enqueue(AsyncLogDetail::Entry&& entry) { + bool notify = false; + { + std::unique_lock lock(mutex); + space.wait(lock, [&] { return bytes == 0 || bytes + entry.cost <= Capacity; }); + bytes += entry.cost; + peak = std::max(peak, bytes); + pending.push_back(std::move(entry)); + queued.fetch_add(1, std::memory_order_release); + notify = sleeping; + sleeping = false; + } + if (notify) wake.notify_one(); + } + + void Run() noexcept { + std::deque batch; + std::string buffer; + for (;;) { + { + std::unique_lock lock(mutex); + sleeping = true; + wake.wait_for(lock, Tick, [&] { return !pending.empty(); }); + sleeping = false; + batch.swap(pending); + bytes = 0; + } + space.notify_all(); + if (!batch.empty()) { + const auto start = Clock::now(); + const auto count = batch.size(); + try { + Drain(batch, buffer); + } catch (...) { + buffer.clear(); + } + batch.clear(); + loggerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); + { + std::lock_guard lock(mutex); + finished.fetch_add(count, std::memory_order_release); + } + idle.notify_all(); + } + try { + Housekeeping(); + } catch (...) { + } + } + } + + void Drain(std::deque& batch, std::string& buffer) { + std::FILE* current = nullptr; + buffer.clear(); + std::uint64_t outLines = 0; + std::uint64_t errLines = 0; + std::uint64_t written = 0; + const auto emit = [&] { + if (current != nullptr && !buffer.empty()) { + written += buffer.size(); + WriteNow(current, buffer.data(), buffer.size()); + } + buffer.clear(); + }; + for (auto& entry : batch) { + if (entry.stream != current) { + emit(); + current = entry.stream; + } + if (entry.deferred) { + if (entry.micros >= 0) AppendTimestamp(buffer, entry.micros); + entry.deferred->Format(buffer); + } else { + buffer += entry.text; + } + (entry.stream == stderr ? errLines : outLines) += 1; + if (buffer.size() >= 65536) emit(); + } + emit(); + stdoutLines.fetch_add(outLines, std::memory_order_relaxed); + stderrLines.fetch_add(errLines, std::memory_order_relaxed); + loggedBytes.fetch_add(written, std::memory_order_relaxed); + } + + void Housekeeping() { + const auto now = Clock::now(); + std::vector summaries; + { + std::lock_guard lock(coalesceMutex); + for (auto it = windows.begin(); it != windows.end();) { + if (now - it->second.first >= CoalesceWindow) { + const auto summary = Summary(it->first, it->second.suppressed); + if (!summary.empty()) summaries.push_back(summary); + it = windows.erase(it); + } else { + ++it; + } + } + } + for (const auto& summary : summaries) WriteNow(stderr, summary.data(), summary.size()); + if (now - lastStats < StatsWindow) return; + const double seconds = std::chrono::duration(now - lastStats).count(); + lastStats = now; + const auto outLines = stdoutLines.exchange(0, std::memory_order_relaxed); + const auto errLines = stderrLines.exchange(0, std::memory_order_relaxed); + const auto written = loggedBytes.exchange(0, std::memory_order_relaxed); + const auto caller = callerNanoseconds.exchange(0, std::memory_order_relaxed); + const auto logger = loggerNanoseconds.exchange(0, std::memory_order_relaxed); + const auto coalesced = coalescedTotal.exchange(0, std::memory_order_relaxed); + std::size_t high = 0; + { + std::lock_guard lock(mutex); + high = peak; + peak = bytes; + } + if (!statsEnabled || outLines + errLines + coalesced == 0) return; + char line[512]; + const int size = std::snprintf(line, sizeof(line), "[log] %.1f s: %llu lines (%llu stdout, %llu stderr), %.1f KiB written, %llu repeats coalesced; callers spent %.2f ms queueing and formatting (%.3f%% of one thread), logger thread %.2f ms, queue peak %.1f KiB\n", seconds, static_cast(outLines + errLines), static_cast(outLines), static_cast(errLines), static_cast(written) / 1024.0, static_cast(coalesced), static_cast(caller) / 1e6, static_cast(caller) / 1e9 / seconds * 100.0, static_cast(logger) / 1e6, static_cast(high) / 1024.0); + if (size > 0) WriteNow(stderr, line, std::min(static_cast(size), sizeof(line) - 1)); + } + + bool asynchronous = true; + bool statsEnabled = true; + std::mutex mutex; + std::condition_variable wake; + std::condition_variable space; + std::condition_variable idle; + std::deque pending; + std::size_t bytes = 0; + std::size_t peak = 0; + bool sleeping = false; + std::atomic queued{0}; + std::atomic finished{0}; + std::atomic abandoned{false}; + std::mutex coalesceMutex; + std::map windows; + Clock::time_point lastStats; + std::atomic stdoutLines{0}; + std::atomic stderrLines{0}; + std::atomic loggedBytes{0}; + std::atomic callerNanoseconds{0}; + std::atomic loggerNanoseconds{0}; + std::atomic coalescedTotal{0}; +}; + +inline void InstallAsyncLog_nid_no_patch() { + static_cast(AsyncLog::Get()); +} + +inline void FlushAsyncLog_nid_no_patch() { + AsyncLog::Get().Flush(); +} + +inline void FlushAsyncLogAtEnd_nid_no_patch() { + AsyncLog::FlushAtEnd(); +} + +std::unique_lock HoldAsyncLog(); + +template +void LogCoalescedStderr_nid_no_patch(const char* format, TArgs... args) { + char small[1024]; + const int size = std::snprintf(small, sizeof(small), format, args...); + if (size < 0) return; + std::string line = static_cast(size) < sizeof(small) ? std::string(small, static_cast(size)) : std::string(static_cast(size), '\0'); + if (static_cast(size) >= sizeof(small)) std::snprintf(line.data(), line.size() + 1, format, args...); + if (!AsyncLog::Get().Admit(line)) return; + std::fwrite(line.data(), 1, line.size(), stderr); + std::fflush(stderr); +} + +} + +#define AGC_LOG_DEFERRED(stream, fmt, ...) \ +::AgcDriver::AsyncLog::Get().Defer(stream, "[%s:%d %s] " fmt "\n", ::AgcDriver::LogLiteral{_APS5_FILE_}, __LINE__, ::AgcDriver::LogLiteral{TrimNidPostfix(__func__)}, __VA_ARGS__) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index d43ee2e18a..685976d255 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -22,6 +22,7 @@ struct DisplayBuffer { std::uint32_t pitchInPixel = 0; std::uint64_t dccAddress = 0; std::uint64_t dccClearColor = 0; + std::uint32_t alignment = 65536; }; std::size_t DisplayBufferSize(const DisplayBuffer& buffer); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 76b1a346fc..25bb3d485a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP +#include #include #include "SceTypes.hpp" #include "SceShaders.hpp" @@ -17,6 +18,7 @@ extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); extern "C" void AgcDriverShutdown_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); +extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive); extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 110894795c..b96a4cdf0a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -29,6 +29,7 @@ struct DispatchVariant { std::vector words; std::uint64_t forgetSerial = 0; std::shared_ptr compiled; + std::array meshIndexDescriptor{}; std::shared_ptr shader; @@ -40,6 +41,9 @@ struct DispatchVariant { std::uint32_t flatBinding = NoFlatBinding; std::uint32_t pushOffset = 0; + std::vector runBases; + std::uint64_t relocatableWords = 0; + std::vector> shaderDataPatches; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index ee5ce7844f..23b6231072 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWCACHE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWCACHE_HPP +#include "UserDataUse.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include @@ -30,6 +31,7 @@ struct DrawProgram { bool merged = false; bool mergedPointerRequired = false; std::uint32_t mergedPointer = 0; + std::uint32_t hiddenUserWords = 0; }; struct DrawDecode { @@ -118,6 +120,36 @@ struct StageCapture { std::vector regions; std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; + ShaderRecompiler::SrtUserDataUse userDataUse; +}; + +struct RelocatableUserWord { + std::uint32_t reg; + std::vector> reads; +}; + +struct DrawRelocation { + std::vector words; + std::vector banned; +}; + +struct DrawUserPatch { + std::size_t program; + std::uint32_t index, value; +}; + +struct DrawRelocationPending { + std::uint64_t key = 0, alias = 0, identity = 0; +}; + +struct DrawRelocationTrial { + static constexpr std::uint32_t Window = 1024, MinimumHitShare = 16, IdleDraws = 16384, ProbationAdmissions = 2; + std::uint32_t lookups = 0, hits = 0, idle = 0, admissions = 0; + bool productive = false; +}; + +struct DrawRelocationCounters { + std::uint64_t lookups = 0, donors = 0, refused = 0, hits = 0, evicted = 0; }; bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry& registry); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index d89415137c..ef6a3e2e7d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -26,12 +26,16 @@ public: static DrawPipeline& Queue0(); static std::size_t Depth(); + static std::size_t QueuedItems(); static bool& Active(); static std::atomic& EpochToken(); static void FollowEpoch(std::uint64_t token); + static unsigned Committers(); void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); + void Stop(); + static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode); bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; } std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); @@ -58,6 +62,7 @@ private: std::uint32_t idleWaiters = 0; bool committerWaiting = false; bool stopped = false; + bool closing = false; std::thread thread; std::uint64_t commits = 0; std::uint64_t commitNs = 0; @@ -67,6 +72,7 @@ private: std::array(DrainReason::Count)> drains{}; std::array(DrainReason::Count)> drainWaitNs{}; std::array drainOpcodes{}; + std::array, 257> drainRequests{}; std::uint64_t depthSum = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index a2dad48ca7..84c2a17592 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -49,6 +49,7 @@ public: ~Driver(); void Shutdown(); void WaitIdle(); + std::uint64_t DispatchesAdoptedAhead() const; void CheckFailure(); void ReportFailure(std::exception_ptr error); void Submit(const Packet* packet, std::uint32_t queue); @@ -74,6 +75,7 @@ public: }; DispatchCacheFootprint DispatchCacheSize(); static std::uint64_t DispatchCacheBytes(); + std::shared_ptr RegisteredShaders(); private: friend class SampledReadScope; @@ -101,6 +103,7 @@ private: static bool& completionDeferred(); void deferCompletion(const Submission& submission); void submitOpenWork(); + void wakeQueueZeroForCompletions(); void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span labelWrites); static bool waitFree(const Submission& submission); bool queue0Before(std::uint64_t received) const; @@ -110,6 +113,7 @@ private: static bool stampValidate(); static bool dataHits(); static bool verifyDataHits(); + static bool pipelineDispatch(); static std::size_t dispatchVariants(); static bool insertCompare(); static std::size_t dispatchCacheEntries(); @@ -141,8 +145,16 @@ private: void maybeReportDrawCache(bool profile); void reportDrawCache(DrawEntryCounters& counters, bool profile); std::string profileDrawCacheLines(const DrawEntryCounters& counters); - static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr, std::array* components = nullptr); + static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr, std::array* components = nullptr, const DrawRelocation* relocation = nullptr, std::uint64_t* alias = nullptr); void traceDrawKeyLookup(const std::array& components, bool cacheMiss); + static std::uint64_t drawIdentityKey(const QueueState& queue, const ShaderRegistry& registry); + static bool drawRelocationEnabled(); + static std::uint64_t relocatedContentKey(const RelocatableUserWord& word, std::uint32_t value, bool& readable); + static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords); + static void applyShaderDataPatches(DispatchVariant& variant); + void learnPendingRelocation(); + void relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr& entry, std::shared_ptr& decode, std::vector& patches); + void reportDrawRelocation(); static bool sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b); static bool sameDecode(const DrawDecode& a, const DrawDecode& b); std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission); @@ -184,6 +196,7 @@ private: static bool traceCopy(); void traceCopyPending(const char* what, std::uint64_t address, std::size_t bytes, std::uint64_t source, std::uint64_t destination, std::size_t copyBytes, const std::shared_ptr& localDevice); static void countIndirect(int path, double readMs); + static int indirectArgumentsPath(std::span packet, std::uint64_t arguments); void dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission); static bool& sampledRead(); static bool writeEvidenceEnabled(); @@ -346,6 +359,11 @@ private: std::unordered_map drawShapes; std::unordered_set traceShapes, traceKeysSeen, traceKeysEvicted; std::list drawOrder; + std::unordered_map> drawRelocations; + std::unordered_map> drawAliases; + DrawRelocationPending drawRelocationPending; + std::unordered_map drawRelocationTrials; + DrawRelocationCounters drawRelocationCounters; std::mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; std::optional drawKeySightings; @@ -399,6 +417,7 @@ private: std::atomic evidenceReads{0}; std::atomic evidenceValidations{0}; + std::atomic dispatchesAdoptedAhead{0}; std::mutex writtenBuffersMutex; std::deque writtenBuffers; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp index b5e1b03310..85cc30100c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp @@ -11,6 +11,7 @@ namespace AgcDriver::DriverDetail { std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact); +std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded); bool ExactDrawWrites(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp new file mode 100644 index 0000000000..a29def6cff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp @@ -0,0 +1,40 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONRING_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONRING_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +inline constexpr std::uint32_t SubmissionQueueEnd = 0x58; + +std::size_t SubmissionRingCapacity(); + +std::chrono::seconds StallWatchdogPeriod(); + +bool ReadSubmissionRingSettings(); + +void PrepareSubmissionRing(std::uint32_t queue); + +void RecordSubmission(std::uint32_t queue, std::uint64_t serial, std::span commands, const void* guest, bool rewindTail); + +void DumpSubmissionRings(std::string_view reason); + +class StallWatch { +public: + explicit StallWatch(std::uint32_t queue) : queue(queue) {} + void Progressed() { since = {}; } + void Check(const char* waiter, std::uint64_t value); + +private: + std::uint32_t queue; + std::chrono::steady_clock::time_point since{}; + bool dumped = false; +}; + +} + +#endif 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 60a9fee237..ff30cce469 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -117,7 +117,9 @@ void SubmitRegistrationPreparation(std::function task, const PreparedSha void PrioritizeRegistrationPreparation(PreparedShaders& prepared); void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function task); +void AwaitLibraryPrebuilds(); void CloseRegistrationPreparation(); +unsigned RegistrationPreparationThreads(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp new file mode 100644 index 0000000000..fd2a0317b9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp @@ -0,0 +1,60 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct FrameDumpIoFailure { + const char* operation = nullptr; + int error = 0; + explicit operator bool() const noexcept { return operation != nullptr; } +}; + +inline FrameDumpIoFailure WriteBmpStream_nid_no_patch(std::FILE* file, std::uint32_t width, std::uint32_t height, std::span pixels) noexcept { + const std::uint32_t imageBytes = width * height * 4; + const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; + const std::uint16_t magic = 0x4d42; + const std::uint32_t fileBytes = 54 + imageBytes; + const auto write = [&](const void* data, std::size_t size, const char* operation) { + errno = 0; + if (std::fwrite(data, 1, size, file) != size) return FrameDumpIoFailure{operation, errno}; + return FrameDumpIoFailure{}; + }; + if (const auto failure = write(&magic, 2, "header")) return failure; + if (const auto failure = write(&fileBytes, 4, "header")) return failure; + if (const auto failure = write(header + 1, 48, "header")) return failure; + return write(pixels.data(), pixels.size(), "pixels"); +} + +inline FrameDumpIoFailure WriteBmpAndClose_nid_no_patch(std::FILE* file, std::uint32_t width, std::uint32_t height, std::span pixels) noexcept { + auto failure = WriteBmpStream_nid_no_patch(file, width, height, pixels); + errno = 0; + if (std::fclose(file) != 0 && !failure) failure = {"close", errno}; + return failure; +} + +class FrameDumpOutput { +public: + bool Enabled_nid_no_patch() const noexcept { return enabled.load(std::memory_order_relaxed); } + + bool Write_nid_no_patch(const char* path, std::uint32_t width, std::uint32_t height, std::span pixels) noexcept { + if (!Enabled_nid_no_patch()) return false; + errno = 0; + auto* file = std::fopen(path, "wb"); + const auto failure = file != nullptr ? WriteBmpAndClose_nid_no_patch(file, width, height, pixels) : FrameDumpIoFailure{"open", errno}; + if (!failure) return true; + if (enabled.exchange(false, std::memory_order_relaxed)) { + std::fprintf(stderr, "[gpu] optional BMP dumps disabled: %s failed (errno=%d)\n", failure.operation, failure.error); + } + return false; + } + +private: + std::atomic enabled{true}; +}; + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp new file mode 100644 index 0000000000..6c4b8213ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp @@ -0,0 +1,61 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP + +#include "prx/libc/include/CpuTopology.hpp" +#include +#include +#include +#include +#include +#include +#if defined(__x86_64__) || defined(_M_X64) +#include +#endif + +namespace AgcDriver { + +inline void SpinPause() { +#if defined(__x86_64__) || defined(_M_X64) + _mm_pause(); +#endif +} + +inline unsigned UsableProcessors() { + const auto count = static_cast(std::bitset<64>(CpuTopology::Get().process).count()); + return count != 0 ? count : std::thread::hardware_concurrency(); +} + +inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned usableProcessors) { + constexpr std::chrono::microseconds automaticWindow{150}; + constexpr unsigned automaticProcessorsAbove = 8; + constexpr long long longestWindowUs = 10000; + if (configured != nullptr) return std::chrono::microseconds(std::clamp(std::strtoll(configured, nullptr, 10), 0, longestWindowUs)); + return usableProcessors > automaticProcessorsAbove ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero(); +} + +inline std::chrono::nanoseconds HandoffSpinWindow() { + static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), UsableProcessors()); + return window; +} + +template +bool SpinUntil(std::chrono::nanoseconds window, TReady&& ready) { + if (ready()) return true; + if (window.count() <= 0) return false; + const auto until = std::chrono::steady_clock::now() + window; + do { + SpinPause(); + if (ready()) return true; + } while (std::chrono::steady_clock::now() < until); + return false; +} + +template +void LockSpinning(TMutex& mutex, std::chrono::nanoseconds window) { + if (window.count() > 0 && SpinUntil(window, [&] { return mutex.try_lock(); })) return; + mutex.lock(); +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index 58fde0469b..f902632e1a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -78,6 +78,7 @@ std::uint32_t ReadDrawCount(const DrawParameters::IndirectDraw& indirect); inline bool IndirectDrawOpcode(std::uint32_t opcode) { return opcode == 0x24 || opcode == 0x25 || opcode == 0x2c || opcode == 0x38; } inline bool DrawOpcode(std::uint32_t opcode) { return opcode == 0x27 || opcode == 0x2d || opcode == 0x35 || IndirectDrawOpcode(opcode); } +std::string_view KnownName(std::uint32_t header); std::string Name(std::uint32_t header); // A PM4 type-2 packet is a one-dword filler (command-buffer padding); type 3 and type 0 carry a // dword count in bits 29:16. Type 1 is undefined. diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 9a4dc16db6..9ba265842a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp @@ -47,7 +47,7 @@ struct Recipe { std::uint64_t dataWordsHash = 0; // The template's PresyncSurfaces() as of the attach and the host-import identity they were // computed under (a surface is CPU-read only while its memory has no import). - std::vector> presyncSurfaces; + std::vector presyncSurfaces; Graphics::HostImportsProof presyncProof; // Both false for every recipe (only reusable objects get one); kept for the asserts. bool needsCompletion = false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 87800734a3..98bd5ca486 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -30,6 +30,8 @@ struct ComputePipelineObjects; struct RecipeHit; // The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch). struct RecordedDispatch; +// The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline). +struct ComputePipelineObjects; class VulkanDevice { public: @@ -37,6 +39,14 @@ public: struct CompileOnly {}; explicit VulkanDevice(CompileOnly); ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache = {}); + // Builds the pipeline of a journaled compute result ahead of its first dispatch (the journal + // replay's hook, on its threads): false when the device already holds a pipeline for the module. + bool ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); + // Starts the journal replay if the device has not (a headless device starts it at its first + // draw or dispatch) and joins it (tests; the teardown joins it anyway). + void WaitForPipelineJournalReplay(); + // Compute pipelines this device built, by dispatches and by the journal replay (tests). + std::uint64_t ComputePipelinesCreated() const; ~VulkanDevice(); VulkanDevice(const VulkanDevice&) = delete; VulkanDevice& operator=(const VulkanDevice&) = delete; @@ -48,6 +58,7 @@ public: const std::uint32_t* WorkGroupCountLimit() const; std::uint32_t VendorId() const; ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const; + bool ComputeFullSubgroups() const; // Distinguishes this device from every earlier one in the process (a memo keyed by the device // cannot rely on the pointer, which a replacement may reuse). std::uint64_t Serial() const { return serial; } @@ -58,6 +69,7 @@ public: // three-step form below so the GPU wait happens without the mutex. void WaitIdle(); void PrepareForReplacement(); + void startJournalReplay(); bool ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Sends recorded work to the GPU without waiting for it. With `reapFirst` it first retires batches // that already finished, so the in-flight list stays short (APS5_NO_OPPORTUNISTIC_REAP=1 skips @@ -173,6 +185,10 @@ public: std::uint64_t created, underGpuMutex; }; static ComputePipelineCreation ComputePipelineCounts(); + struct DispatchMoveCount { + std::uint64_t checked, exact, moved, refused, filtered, rebuilt, kept, served; + }; + static DispatchMoveCount DispatchMoveCounts(); void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable); void* Window() const; void Resize(std::uint32_t width, std::uint32_t height); @@ -232,6 +248,10 @@ public: double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs; }; static PresentStatistics PresentCounts(); + struct PresyncStatistics { + std::uint64_t checked, presyncs, keyChecks, keyWaits; + }; + static PresyncStatistics PresyncCounts(); // Stage A of a dispatch's resource build, run WITHOUT GuestMemory::GpuMutex before Dispatch or // DispatchIndirect: the binding plan, data buffers, imports already serving the guest buffers, // read-only copies and the descriptor set (see ShaderResources). The dispatch completes it @@ -316,12 +336,16 @@ private: // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; + VkPipelineCreateFlags computePipelineFlags() const; + std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const; + VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const; + std::shared_ptr createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. - std::uint64_t presync(std::span> surfaces); + std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); // A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or // the counts read on the CPU (the record then continues as a direct dispatch). diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 6137a82dcc..4bdf335e98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -94,7 +94,7 @@ void ComputePipelineCapture::Validate() const { RequireCapture(mainEntry, "compute main entry point missing"); RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name"); RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags"); - RequireCapture(stageFlags == 0, "unsupported shader stage flags"); + RequireCapture(stageFlags == 0 || (stageFlags == VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT && requiredSubgroupSize != 0), "unsupported shader stage flags"); RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size"); RequireCapture(sets.size() <= 8, "too many descriptor sets"); std::size_t totalBindings = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index e540fe0ad9..6f7dd42ade 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -130,7 +130,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { throw std::runtime_error(message); } require(!halfFloat || buffer.dccAddress == 0, "VideoOut: DCC metadata on a 16-bit float display buffer is not implemented"); - require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment"); + require(buffer.alignment == 65536u || (buffer.alignment == 4096u && buffer.tilingMode == 1), "VideoOut: 4 KiB display buffer alignment is only supported for linear buffers"); + require(buffer.address != 0 && (buffer.address & (buffer.alignment - 1u)) == 0, buffer.alignment == 65536u ? "VideoOut: display buffer requires 64 KiB alignment" : "VideoOut: display buffer requires 4 KiB alignment"); require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode"); require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported"); require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata"); @@ -249,7 +250,7 @@ std::vector ReadDisplayBuffer(const DisplayBuffer& buffer) { std::fprintf(stderr, "[gpu] scanout display buffer 0x%llx %ux%u format 0x%016llx at %.1f s\n", static_cast(buffer.address), buffer.width, buffer.height, static_cast(buffer.pixelFormat), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); } GuestMemory::FlushGpuWrites(buffer.address, size); - GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, 65536); + GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, buffer.alignment); const auto* source = reinterpret_cast(buffer.address); return DecodeDisplayBuffer(buffer, {source, size}); } @@ -267,7 +268,7 @@ std::vector DisplayBufferTexels(const DisplayBuffer& buffer, std::spa std::vector ReadDisplayBufferTexels(const DisplayBuffer& buffer) { const auto size = DisplayBufferSize(buffer); GuestMemory::FlushGpuWrites(buffer.address, size); - GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, 65536); + GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, buffer.alignment); return DisplayBufferTexels(buffer, {reinterpret_cast(buffer.address), size}); } 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 366bb010ed..b506cff5c1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -2,9 +2,11 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -15,6 +17,47 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool DispatchItemsEnabled() { + static const bool enabled = [] { + const char* text = std::getenv("APS5_PIPELINE_DISPATCH"); + return text != nullptr && text[0] != '\0' && text[0] != '0'; + }(); + return enabled; +} + +enum class ItemFallback : std::size_t { Miss, Reads, NoRecipe, Writes, Verify, Count }; +constexpr std::array(ItemFallback::Count)> ItemFallbackNames{"miss", "reads", "no recipe", "writes", "verify"}; +std::array, static_cast(ItemFallback::Count)> itemFallbacks{}; +std::atomic itemCommits{0}; +std::atomic itemRebuilds{0}; + +void NoteItemFallback(ItemFallback reason) { + itemFallbacks[static_cast(reason)].fetch_add(1, std::memory_order_relaxed); +} + +void ReportDispatchItems(bool profile) { + static auto lastReport = std::chrono::steady_clock::now(); + if (!profile) return; + const auto now = std::chrono::steady_clock::now(); + if (now - lastReport < std::chrono::seconds(10)) return; + lastReport = now; + std::string drained; + for (std::size_t i = 0; i < ItemFallbackNames.size(); ++i) { + char item[48]; + std::snprintf(item, sizeof(item), " %s %llu", ItemFallbackNames[i], static_cast(itemFallbacks[i].load(std::memory_order_relaxed))); + drained += item; + } + std::fprintf(stderr, "[dispatch-item] %llu commit items (%llu recipes rebuilt on the committer); drained instead, cumulative:%s\n", static_cast(itemCommits.load(std::memory_order_relaxed)), static_cast(itemRebuilds.load(std::memory_order_relaxed)), drained.c_str()); +} + +} + +bool Driver::pipelineDispatch() { + return DispatchItemsEnabled(); +} + void Driver::dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments) { PerformanceTimer timing("Driver.Dispatch"); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(readRegister(queue.shader, 0x20d) & 0xffu) << 40u); @@ -72,6 +115,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (indirectArguments == 0 && (packet[4] & 0x2u) != 0) { partialGroups = Pm4::PartialGroupThreads({packet[1], packet[2], packet[3]}, {readRegister(queue.shader, 0x207), readRegister(queue.shader, 0x208), readRegister(queue.shader, 0x209)}); } + const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); const bool partialLastGroup = partialGroups != std::array{}; if (resolving) { if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; @@ -84,7 +128,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, pendingDispatchPhases().outcome = DispatchOutcome::FillHle; return; } - if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); + if (DrawPipeline::Active() && !pipelineCandidate) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments); @@ -223,6 +267,27 @@ void Driver::dispatch(QueueState& queue, std::span packet, } phaseTiming.Phase(PhaseKey); timing.Mark("decode_key_device_setup"); + bool readsClear = false; + if (pipelineCandidate) { + std::vector reads; + bool entryFound = false; + { + std::lock_guard cacheLock(dispatchCacheMutex); + if (const auto found = dispatchCache.find(key); found != dispatchCache.end()) { + entryFound = true; + for (const auto& variant : found->second->variants) appendEntryRegions(*variant, reads); + } + } + if (!entryFound || noDispatchCache || stampValidate()) { + NoteItemFallback(ItemFallback::Miss); + } else if (verifyDataHits()) { + NoteItemFallback(ItemFallback::Verify); + } else { + readsClear = std::none_of(reads.begin(), reads.end(), [](const auto& region) { return DrawPipeline::Queue0().Overlaps(region.guestAddress, region.bytes.size()); }); + if (!readsClear) NoteItemFallback(ItemFallback::Reads); + } + if (!readsClear && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); + } if (fromAhead) { compiledResult = std::move(ahead.compiledResult); keepVariant = std::move(ahead.keepVariant); @@ -235,6 +300,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, cached = ahead.cached; validated = ahead.validated; ++groupStats.adopted; + ++dispatchesAdoptedAhead; const auto age = std::chrono::duration(std::chrono::steady_clock::now() - ahead.resolvedAt).count(); groupStats.ageMs += age; groupStats.ageMaxMs = std::max(groupStats.ageMaxMs, age); @@ -243,6 +309,19 @@ void Driver::dispatch(QueueState& queue, std::span packet, lookupDispatch(address, submission, key, noDispatchCache, traceCache, profile, memory, phaseTiming, phaseMs, compiledResult, keepVariant, captured, liveWords, dataHit, cached, validated, missedEntry, missedDiffering); } timing.Mark("lookup_cache"); + std::vector itemWrites; + bool pipelined = false; + if (readsClear) { + if (cached && keepVariant != nullptr && keepVariant->recipe.load(std::memory_order_acquire) != nullptr) { + bool bounded = false; + itemWrites = DispatchWriteRanges(*compiledResult, bounded); + pipelined = bounded; + if (!bounded) NoteItemFallback(ItemFallback::Writes); + } else { + NoteItemFallback(ItemFallback::NoRecipe); + } + if (!pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); + } if (cached) { require(keepVariant != nullptr && keepVariant->shader == registeredShader, "dispatch cache belongs to another registered shader"); captureMs += phaseTiming.Elapsed(); @@ -360,6 +439,64 @@ void Driver::dispatch(QueueState& queue, std::span packet, }; phaseTiming.Phase(PhaseSnapshots); + if (pipelined) { + const auto recipe = keepVariant->recipe.load(std::memory_order_acquire); + const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + const auto queueId = submission.queue; + itemCommits.fetch_add(1, std::memory_order_relaxed); + DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), groups, address, queueId, recipe, keepVariant, dataHit, epoch] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(0x15, queueId); + const auto& compiledCommit = *result; + const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + try { + std::shared_ptr recipeHit = localDevice->PrepareRecipe(recipe, false); + std::shared_ptr builtRecipe; + bool writersNoted = false; + for (;;) { + std::shared_ptr prepared; + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + recordLabelsForPacket(localDevice.get(), queueId); + if (noteWrites && writerKeyedEvidence() && !writersNoted) { + noteWrittenBuffers(address, queueId, compiledCommit); + writersNoted = true; + } + if (recipeHit != nullptr) { + VulkanDevice::IndirectOutcome outcome{0, 0}; + const auto recipeResult = localDevice->DispatchRecipe(compiledCommit, groups[0], groups[1], groups[2], 0, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + if (recipeResult == RecipeOutcome::Rebuild) { + recipeHit = nullptr; + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, false); + itemRebuilds.fetch_add(1, std::memory_order_relaxed); + continue; + } + } else { + localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); + } + break; + } + if (noteWrites && !writerKeyedEvidence()) noteWrittenBuffers(address, queueId, compiledCommit); + if (builtRecipe != nullptr) { + if (dataHit) { + auto own = std::make_shared(*builtRecipe); + own->dataWordsHash = Graphics::ShaderResources::DataWordsHash({ShaderRecompiler::ShaderStage::Compute, keepVariant->compiled.get(), 0}); + builtRecipe = std::move(own); + } + keepVariant->recipe.store(std::move(builtRecipe), std::memory_order_release); + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Attach, false); + } + } catch (const std::exception& error) { + char where[64]; + std::snprintf(where, sizeof(where), "compute shader 0x%llx: ", static_cast(address)); + throw std::runtime_error(where + std::string(error.what())); + } + }, std::move(itemWrites)); + ReportDispatchItems(profile); + return; + } + std::shared_ptr recipeHit; if (cached && keepVariant != nullptr && !stampValidate()) { recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); 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 441cbdf3c7..91cb9cf4c0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -35,6 +35,10 @@ bool& Driver::gateOpened() { return opened; } +std::uint64_t Driver::DispatchesAdoptedAhead() const { + return dispatchesAdoptedAhead.load(std::memory_order_relaxed); +} + void Driver::clearResolvedAhead() { auto& resolved = resolvedAhead(); if (resolved.empty()) return; @@ -146,7 +150,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l const auto argumentsEnd = arguments + 3u * sizeof(std::uint32_t); const bool written = std::any_of(writes.begin(), writes.end(), [&](const auto& range) { return range.first < argumentsEnd && arguments < range.second; }) || std::any_of(labels.begin(), labels.end(), [&](const RunLabel& label) { return label.address < argumentsEnd && arguments < label.address + label.size; }); - if (unknownWrites || written) { + if ((unknownWrites || written) && indirectArgumentsPath(packet, arguments) != IndirectGpu) { ++stats.writtenBefore; break; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index 9be8199572..f567a93e2e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -23,14 +23,18 @@ void Driver::countIndirect(int path, double readMs) { std::fprintf(stderr, "[indirect] gpu-side %llu, cpu-side %llu (thread dimensions %llu, fill kernel %llu, pending image results %llu, pending label or copied write %llu, not imported %llu, misaligned %llu, disabled %llu, device memory LDS %llu); CPU argument reads took %.1f s\n", static_cast(counts[IndirectGpu]), static_cast(cpu), static_cast(counts[IndirectThreadDimensions]), static_cast(counts[IndirectFillKernel]), static_cast(counts[IndirectPendingImage]), static_cast(counts[IndirectCopiedWrite]), static_cast(counts[IndirectNotImported]), static_cast(counts[IndirectMisaligned]), static_cast(counts[IndirectDisabled]), static_cast(counts[IndirectWorkgroupMemory]), readWaitedMs / 1000); } -void Driver::dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission) { +int Driver::indirectArgumentsPath(std::span packet, std::uint64_t arguments) { static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; + if (!gpuIndirect) return IndirectDisabled; + if ((packet.back() & 0x20u) != 0) return IndirectThreadDimensions; + if (arguments % 4 != 0) return IndirectMisaligned; + return IndirectGpu; +} + +void Driver::dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission) { const auto arguments = Pm4::DispatchArgumentAddress(packet, queue); const auto initiator = packet.back(); - int path = IndirectGpu; - if (!gpuIndirect) path = IndirectDisabled; - else if ((initiator & 0x20u) != 0) path = IndirectThreadDimensions; - else if (arguments % 4 != 0) path = IndirectMisaligned; + const int path = indirectArgumentsPath(packet, arguments); if (path != IndirectGpu) { if (!resolvingAhead()) { const auto& resolved = resolvedAhead(); 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 a6421e61c9..81bfc0d016 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -77,10 +77,26 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::shared_ptr decode; std::shared_ptr dataEntry; std::uint64_t shapeKey = 0; + std::uint64_t drawIdentity = 0, drawAlias = 0; + std::vector userPatches; if (registerKey) { const auto keyStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr); - std::lock_guard cacheLock(drawCacheMutex); + std::shared_ptr relocation; + bool aliasFirst = false; + if (drawRelocationEnabled()) { + drawIdentity = drawIdentityKey(queue, *submission.shaders); + std::lock_guard cacheLock(drawCacheMutex); + learnPendingRelocation(); + const auto known = drawRelocations.find(drawIdentity); + if (known != drawRelocations.end()) { + auto& trial = drawRelocationTrials[drawIdentity]; + if (trial.idle != 0) --trial.idle; + else relocation = known->second; + aliasFirst = relocation != nullptr && trial.productive; + } + } + drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr, aliasFirst ? relocation.get() : nullptr, aliasFirst ? &drawAlias : nullptr); + std::unique_lock cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; ++drawEntryCounters.registerKeyLookups; if (profile) drawEntryCounters.keyUs += std::chrono::duration(std::chrono::steady_clock::now() - keyStart).count(); @@ -90,6 +106,22 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe decode = entry->decode; } else { ++drawEntryCounters.absent; + if (relocation != nullptr && !aliasFirst) { + cacheLock.unlock(); + drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), nullptr, nullptr, relocation.get(), &drawAlias); + cacheLock.lock(); + } + if (drawIdentity != 0) drawRelocationPending = {drawKey, drawAlias, drawIdentity}; + if (drawAlias != 0) relocateDrawEntry(queue, drawAlias, entry, decode, userPatches); + if (relocation != nullptr) { + auto& trial = drawRelocationTrials[drawIdentity]; + ++trial.lookups; + if (trial.lookups >= DrawRelocationTrial::Window) { + trial.productive = trial.hits * DrawRelocationTrial::MinimumHitShare >= trial.lookups; + if (!trial.productive) trial.idle = DrawRelocationTrial::IdleDraws; + trial.lookups = trial.hits = 0; + } + } if (traceDrawCache()) { auto& counters = drawEntryCounters; if (traceKeysEvicted.contains(drawKey)) ++counters.absentEvicted; @@ -97,7 +129,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe else if (traceShapes.contains(shapeKey)) ++counters.absentUserWords; else ++counters.absentNewRegisters; } - if (drawDataHits() && !drawParameters.indirect) { + if (decode == nullptr && drawDataHits() && !drawParameters.indirect) { if (const auto shape = drawShapes.find(shapeKey); shape != drawShapes.end()) { const auto kept = drawCache.find(shape->second); if (kept != drawCache.end() && kept->second->decode != nullptr && kept->second->shape == shapeKey) dataEntry = kept->second; @@ -113,6 +145,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe maybeReportDrawCache(profile); } if (decode != nullptr && !DecodeProgramsCurrent(*decode, *submission.shaders)) { + userPatches.clear(); std::lock_guard cacheLock(drawCacheMutex); eraseDrawEntry(drawKey, entry); ++drawEntryCounters.absent; @@ -144,6 +177,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& graphics = decode->state; auto pixel = decode->pixel; std::vector programs = decode->programs; + for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; @@ -238,6 +272,25 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe bool verifyHit = false; const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData); + for (std::size_t i = 0; i < matched.size(); ++i) { + if (matched[i] == nullptr || matched[i]->shaderDataPatches.empty()) continue; + applyShaderDataPatches(*matched[i]); + if (stageData[i].compiled != nullptr) stageData[i].compiled = patchDataWords(*matched[i], stageData[i].words); + } + const auto relocatedDecode = decode; + if (!userPatches.empty()) { + std::lock_guard cacheLock(drawCacheMutex); + if (drawHit || verifyHit) { + ++drawRelocationCounters.hits; + ++drawRelocationTrials[drawIdentity].hits; + } + if (profile && drawRelocationCounters.lookups >= 100000) reportDrawRelocation(); + } + if (!userPatches.empty() && !drawHit) { + auto current = std::make_shared(*decode); + for (const auto& patch : userPatches) current->programs[patch.program].userData[patch.index] = patch.value; + decode = std::move(current); + } const bool patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; }); std::uint64_t stageDataVerified = 0; @@ -383,15 +436,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe phaseTiming.Phase(DrawRowVectors); require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs"); auto rectangle = DrawRectangle(*programs[0].snapshot, programs[1].snapshot, programResults[0]->variantId, programResults[1]->variantId, localDevice->Target()); + auto& control = graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? rectangle.lastVertexControl : rectangle.control; if (rectListBuilt) { - results[rectIndex] = std::move(rectangle.control); + results[rectIndex] = std::move(control); results[rectIndex + 1] = std::move(rectangle.evaluation); phaseTiming.Phase(DrawRowRectList); return; } require(stages.size() == 2, "rect-list requires vertex and fragment programs"); rectIndex = results.size(); - results.push_back(std::move(rectangle.control)); + results.push_back(std::move(control)); results.push_back(std::move(rectangle.evaluation)); stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[rectIndex], 0}, {Stage::TessellationEvaluation, &results[rectIndex + 1], 0}}); rectListBuilt = true; 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 e7b4e66382..948bc83d91 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -102,7 +102,22 @@ bool Driver::admitDrawKey(std::uint64_t key, std::uint64_t shape) { std::lock_guard cacheLock(drawCacheMutex); if (drawCache.contains(key)) return true; if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); - const bool seen = drawKeySightings->SeenBefore(key); + bool seen = drawKeySightings->SeenBefore(key); + const auto& pending = drawRelocationPending; + if (pending.key == key && pending.identity != 0) { + if (pending.alias != 0) { + const auto trial = drawRelocationTrials.find(pending.identity); + if (drawKeySightings->SeenBefore(pending.alias ^ 0x9e3779b97f4a7c15ull) && trial != drawRelocationTrials.end()) { + auto& state = trial->second; + if (state.productive || state.hits != 0 || state.admissions < DrawRelocationTrial::ProbationAdmissions) { + if (!state.productive && state.hits == 0) ++state.admissions; + seen = true; + } + } + } else if (!drawRelocations.contains(pending.identity)) { + if (drawKeySightings->SeenBefore(pending.identity ^ 0xc2b2ae3d27d4eb4full)) seen = true; + } + } if (seen || shape == 0 || drawShapes.contains(shape)) return seen; if (!drawShapeSightings) drawShapeSightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); return drawShapeSightings->SeenBefore(shape); @@ -125,7 +140,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectorstages[i]; - const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); + const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->meshIndexDescriptor == fresh[i]->meshIndexDescriptor && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); if (present != variants.end()) { ++counters.present; fresh[i] = *present; 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 5c049b74a6..616d63c690 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include #include #include #include @@ -55,6 +56,7 @@ std::shared_ptr Driver::materializeDraw }(); stageCapture.regions = shaderMemory.TakeRecentRegions(); + stageCapture.userDataUse = capture->userDataUse; recompiled[i] = true; memory = shaderMemory.Regions(); @@ -148,6 +150,9 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue; auto variant = std::make_shared(); variant->compiled = stageCapture.compiled; + if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) { + std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin()); + } variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; @@ -156,13 +161,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); for (const auto& read : decodeReads[i]) regions.push_back({read.address, std::as_bytes(std::span(read.bytes))}); std::stable_sort(regions.begin(), regions.end(), [](const ShaderRecompiler::MemoryRegion& a, const ShaderRecompiler::MemoryRegion& b) { return a.guestAddress < b.guestAddress; }); - for (const auto& region : regions) { - variant->runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); - const auto count = region.bytes.size() / sizeof(std::uint32_t); - const auto offset = variant->words.size(); - variant->words.resize(offset + count); - std::memcpy(variant->words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); - } + tagVariantRuns(*variant, regions, stageCapture.userDataUse, decodeReads[i].empty() && (!vertexInfos[i] || vertexInfos[i]->resourcesNum == 0), programs[i].hiddenUserWords); if (drawStageDataHits() && stageCapture.capture != nullptr && !stageCapture.capture->readTrace.leaves.empty()) recordStageDataPositions(*variant, *stageCapture.capture, decodeReads[i]); if (traceDrawCache() && entry != nullptr && matched[i] == nullptr && i < entry->stages.size()) { const auto& variants = entry->stages[i]; @@ -174,7 +173,26 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP ++differingRunsChanged; } } - if (verifyHit && matched[i] != nullptr && (matched[i]->runs != variant->runs || matched[i]->words != variant->words)) { + const auto sameContent = [](const DispatchVariant& a, const DispatchVariant& b) { + const auto flatten = [](const DispatchVariant& source) { + std::vector> flat; + std::size_t word = 0; + for (const auto& [begin, end] : source.runs) { + for (auto address = begin; address < end && word < source.words.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, source.words[word++]); + } + std::sort(flat.begin(), flat.end()); + return flat; + }; + return (a.runs == b.runs && a.words == b.words) || flatten(a) == flatten(b); + }; + const auto sameForwardedWords = [](const ShaderRecompiler::RecompileResult& a, const ShaderRecompiler::RecompileResult& b, std::uint32_t hidden) { + if (a.pushConstants.size() != b.pushConstants.size() || a.shaderDataUserWords != b.shaderDataUserWords) return false; + for (std::size_t slot = 0; slot < a.shaderDataUserWords.size() && (slot + 1) * sizeof(std::uint32_t) <= a.pushConstants.size(); ++slot) { + if (a.shaderDataUserWords[slot] >= hidden && std::memcmp(a.pushConstants.data() + slot * sizeof(std::uint32_t), b.pushConstants.data() + slot * sizeof(std::uint32_t), sizeof(std::uint32_t)) != 0) return false; + } + return true; + }; + if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || !sameForwardedWords(*matched[i]->compiled, *variant->compiled, programs[i].hiddenUserWords))) { ++mismatches; static std::atomic reports{0}; if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast(programs[i].binary.codeAddress), matched[i]->runs.size(), matched[i]->words.size(), variant->runs.size(), variant->words.size()); 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 9079eb1c16..598176196b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -35,6 +35,7 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32 program.mergedPointerRequired = pointerRequired; program.firstUserSgpr = 0; program.userData.insert(program.userData.begin(), 8, 0); + program.hiddenUserWords = 8; Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase); Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1); if (staticAbi) return; 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 8308914ff2..c64a860a7a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include namespace AgcDriver::DriverDetail { @@ -21,7 +22,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrpushOffset != cursor) continue; + if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh && !std::equal(variant->meshIndexDescriptor.begin(), variant->meshIndexDescriptor.end(), programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord)) { + if (!anyLayout) outcome = EntryOutcome::Differing; + anyLayout = true; + continue; + } auto& regions = matchedRegions[i]; regions.clear(); appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index b9ddd2c33f..6aaf4ae4bd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include #include #include @@ -14,6 +15,8 @@ namespace { constexpr std::array(DrawPipeline::DrainReason::Count)> DrainNames{"packet", "flush", "labels", "capture", "indirect", "submission"}; +std::atomic runningCommitters{0}; + std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - since).count()); } @@ -36,6 +39,12 @@ std::size_t DrawPipeline::Depth() { return depth; } +std::size_t DrawPipeline::QueuedItems() { + auto& pipeline = Queue0(); + std::lock_guard lock(pipeline.mutex); + return pipeline.items.size(); +} + bool& DrawPipeline::Active() { static thread_local bool active = false; return active; @@ -53,6 +62,10 @@ void DrawPipeline::FollowEpoch(std::uint64_t token) { GuestMemory::BumpCollectEpoch(); } +unsigned DrawPipeline::Committers() { + return runningCommitters.load(std::memory_order_acquire); +} + void DrawPipeline::rethrowFailure() { if (failure == nullptr) return; auto error = std::exchange(failure, nullptr); @@ -63,6 +76,11 @@ void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64 static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; std::unique_lock lock(mutex); rethrowFailure(); + if (closing) { + lock.unlock(); + commit(); + return; + } if (items.size() >= Depth()) { if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline full while the enqueuing thread holds the GPU lock"); const auto start = std::chrono::steady_clock::now(); @@ -76,7 +94,10 @@ void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64 ++enqueued; items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)}); outstanding.fetch_add(1, std::memory_order_release); - if (!thread.joinable()) thread = std::thread([this] { run(); }); + if (!thread.joinable()) { + thread = std::thread([this] { run(); }); + runningCommitters.fetch_add(1, std::memory_order_acq_rel); + } if (committerWaiting) wake.notify_one(); if (!profile) return; const auto now = std::chrono::steady_clock::now(); @@ -113,11 +134,13 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { void DrawPipeline::run() { for (;;) { Commit* commit = nullptr; + SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); { std::unique_lock lock(mutex); committerWaiting = true; - wake.wait(lock, [&] { return !items.empty(); }); + wake.wait(lock, [&] { return !items.empty() || closing; }); committerWaiting = false; + if (items.empty()) break; commit = stopped ? nullptr : &items.front().commit; } const auto start = std::chrono::steady_clock::now(); @@ -149,12 +172,28 @@ void DrawPipeline::run() { outstanding.fetch_sub(1, std::memory_order_release); if (idleWaiters != 0) idle.notify_all(); } + runningCommitters.fetch_sub(1, std::memory_order_acq_rel); +} + +void DrawPipeline::Stop() { + std::thread committer; + { + std::lock_guard lock(mutex); + closing = true; + committer = std::move(thread); + } + wake.notify_all(); + if (!committer.joinable()) return; + if (committer.get_id() == std::this_thread::get_id()) committer.detach(); + else committer.join(); } void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { + drainRequests[std::min(opcode, 0x100)].fetch_add(1, std::memory_order_relaxed); if (!Busy()) return; if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast(reason)) + ", opcode " + std::to_string(opcode) + ")"); const auto start = std::chrono::steady_clock::now(); + SpinUntil(HandoffSpinWindow() * 2, [&] { return !Busy(); }); std::unique_lock lock(mutex); ++idleWaiters; idle.wait(lock, [&] { return items.empty(); }); @@ -166,6 +205,10 @@ void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { rethrowFailure(); } +std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) { + return Queue0().drainRequests[std::min(opcode, 0x100)].load(std::memory_order_relaxed); +} + std::optional DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > 8) return std::nullopt; const auto end = address + bytes; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index d07b070444..56e4801c73 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -33,6 +33,20 @@ const auto& registerMasks() { return masks; } +template +void forEachKeyedUserWord(const QueueState& queue, bool allUserWords, TVisit&& visit) { + 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) visit(it->first, it->second); + }; + 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); +} + } std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental, std::uint64_t* shapeKey, RegisterStateKeyParts* parts) { @@ -86,13 +100,59 @@ std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& re return key; } -std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape, std::array* components) { +std::uint64_t Driver::drawIdentityKey(const QueueState& queue, const ShaderRegistry& registry) { + std::uint64_t key = 0x84222325cbf29ce4ull; + const auto mix = [&](std::uint64_t value) { + key ^= value; + key *= 0x100000001b3ull; + }; + for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { + const auto low = queue.shader.find(base); + const auto high = queue.shader.find(base + 1); + if (low == queue.shader.end() || high == queue.shader.end()) { + mix(0); + continue; + } + mix(low->second); + mix(high->second); + const auto resources = queue.shader.find(base + 3); + mix(resources != queue.shader.end() ? resources->second : 0); + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); + auto it = registry.upper_bound(address); + if (it == registry.begin()) { + mix(1); + continue; + } + --it; + for (const auto& snapshot : it->second) mix(reinterpret_cast(snapshot.get())); + mix(address - it->first); + } + return key == 0 ? 1 : key; +} + +std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape, std::array* components, const DrawRelocation* relocation, std::uint64_t* alias) { + const bool aliased = alias != nullptr && relocation != nullptr && !relocation->words.empty(); + bool readable = true; + std::uint64_t other = 0x6c62272e07bb0142ull; + const auto mixOther = [&](std::uint64_t value) { + other ^= value; + other *= 0x100000001b3ull; + }; static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr; static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr; std::uint64_t shapeKey = 0; RegisterStateKeyParts parts; auto key = RegisterStateKey(queue, allUserWords, true, &shapeKey, components != nullptr ? &parts : nullptr); if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state"); + if (aliased) { + mixOther(shapeKey); + forEachKeyedUserWord(queue, allUserWords, [&](std::uint32_t offset, std::uint32_t value) { + mixOther(offset); + const auto word = std::find_if(relocation->words.begin(), relocation->words.end(), [&](const RelocatableUserWord& candidate) { return candidate.reg == offset; }); + mixOther(word != relocation->words.end() ? relocatedContentKey(*word, value, readable) : value); + }); + mixOther(deviceSerial); + } const auto mix = [&](std::uint64_t value) { key ^= value; key *= 0x100000001b3ull; @@ -123,8 +183,13 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis --it; for (const auto& snapshot : it->second) mix(reinterpret_cast(snapshot.get())); mix(address - it->first); + if (aliased) { + for (const auto& snapshot : it->second) mixOther(reinterpret_cast(snapshot.get())); + mixOther(address - it->first); + } } if (shape != nullptr) *shape = shapeKey; + if (aliased && readable) *alias = other == 0 ? 1 : other; return key; } @@ -170,7 +235,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto sameMesh = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize; + return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize && x->outputPrimitiveSize == y->outputPrimitiveSize; }; const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp new file mode 100644 index 0000000000..da86f3e422 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp @@ -0,0 +1,269 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +namespace { + +constexpr std::size_t MaxAliases = 16384; +constexpr std::size_t MaxContentWords = 64; + +std::uint64_t AlignedWord(std::uint32_t word) { + return word & ~std::uint32_t {3}; +} + +} + +bool Driver::drawRelocationEnabled() { + static const bool enabled = std::getenv("APS5_NO_DRAW_RELOCATION") == nullptr; + return enabled; +} + +std::uint64_t Driver::relocatedContentKey(const RelocatableUserWord& word, std::uint32_t value, bool& readable) { + std::uint64_t hash = 0x9e3779b97f4a7c15ull; + const auto mix = [&](std::uint64_t mixed) { + hash ^= mixed; + hash *= 0x100000001b3ull; + }; + mix(value & 3u); + if (value == 0) { + readable = false; + return hash; + } + std::array content; + for (const auto& [offset, words] : word.reads) { + mix(static_cast(offset)); + mix(words); + for (std::size_t done = 0; done < words; done += MaxContentWords) { + const auto count = std::min(words - done, MaxContentWords); + const auto address = AlignedWord(value) + static_cast(offset) + done * sizeof(std::uint32_t); + if (GuestMemory::CopyMapped(address, std::as_writable_bytes(std::span(content).first(count))) != GuestMemory::Compare::Equal) { + readable = false; + return hash; + } + for (std::size_t i = 0; i < count; ++i) mix(content[i]); + } + } + return hash; +} + +void Driver::tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords) { + std::uint64_t relocatable = eligible && drawRelocationEnabled() ? use.asBase & ~use.asValue : 0; + if (hiddenWords != 0) relocatable &= hiddenWords >= 64 ? 0 : ~((std::uint64_t {1} << hiddenWords) - 1); + std::vector> reads; + const auto covered = [&](std::uint64_t address) { + return std::any_of(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& region) { return address >= region.guestAddress && address - region.guestAddress + sizeof(std::uint32_t) <= region.bytes.size(); }); + }; + if (relocatable != 0) { + reads = use.reads; + std::sort(reads.begin(), reads.end()); + reads.erase(std::unique(reads.begin(), reads.end()), reads.end()); + const auto drop = [&](std::int32_t origin) { + if (origin >= 0) relocatable &= ~(std::uint64_t {1} << origin); + }; + for (std::size_t i = 0; i < reads.size(); ++i) { + if (i + 1 < reads.size() && reads[i].first == reads[i + 1].first) { + drop(reads[i].second); + drop(reads[i + 1].second); + } + if (reads[i].second >= 0 && ((reads[i].first & 3u) != 0 || !covered(reads[i].first))) drop(reads[i].second); + } + } + const auto origin = [&](std::uint64_t address) -> std::int8_t { + const auto found = std::lower_bound(reads.begin(), reads.end(), std::make_pair(address, std::numeric_limits::min())); + if (found == reads.end() || found->first != address || found->second < 0) return -1; + return (relocatable >> found->second & 1u) != 0 ? static_cast(found->second) : std::int8_t {-1}; + }; + variant.relocatableWords = relocatable; + for (const auto& region : regions) { + const auto count = region.bytes.size() / sizeof(std::uint32_t); + const auto offset = variant.words.size(); + variant.words.resize(offset + count); + std::memcpy(variant.words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); + if (relocatable == 0) { + variant.runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); + continue; + } + std::size_t start = 0; + auto current = count != 0 ? origin(region.guestAddress) : std::int8_t {-1}; + for (std::size_t word = 1; word <= count; ++word) { + const auto next = word < count ? origin(region.guestAddress + word * sizeof(std::uint32_t)) : std::int8_t {-2}; + if (next == current) continue; + variant.runs.emplace_back(region.guestAddress + start * sizeof(std::uint32_t), region.guestAddress + word * sizeof(std::uint32_t)); + variant.runBases.push_back(current); + start = word; + current = next; + } + } +} + +namespace { + +template +std::size_t dropEveryOther(TMap& map) { + std::size_t dropped = 0; + bool drop = false; + for (auto it = map.begin(); it != map.end();) { + drop = !drop; + if (!drop) { + ++it; + continue; + } + it = map.erase(it); + ++dropped; + } + return dropped; +} + +} + +void Driver::learnPendingRelocation() { + const auto pending = drawRelocationPending; + drawRelocationPending = {}; + if (pending.key == 0) return; + const auto inserted = drawCache.find(pending.key); + if (inserted == drawCache.end() || inserted->second->decode == nullptr) return; + const auto& entry = *inserted->second; + const auto& programs = entry.decode->programs; + if (entry.stages.size() != programs.size()) return; + const auto known = drawRelocations.find(pending.identity); + auto shape = std::make_shared(); + if (known != drawRelocations.end()) *shape = *known->second; + bool changed = known == drawRelocations.end(); + for (std::size_t i = 0; i < programs.size(); ++i) { + if (entry.stages[i].empty()) continue; + const auto& variant = *entry.stages[i].front(); + const auto& program = programs[i]; + for (std::uint32_t index = program.hiddenUserWords; index < program.userData.size() && index < 64; ++index) { + const auto reg = program.userDataBase + index - program.hiddenUserWords; + const auto held = std::find_if(shape->words.begin(), shape->words.end(), [&](const RelocatableUserWord& word) { return word.reg == reg; }); + if ((variant.relocatableWords >> index & 1u) == 0) { + if (held == shape->words.end()) continue; + shape->words.erase(held); + shape->banned.push_back(reg); + changed = true; + continue; + } + if (held != shape->words.end() || std::find(shape->banned.begin(), shape->banned.end(), reg) != shape->banned.end()) continue; + RelocatableUserWord word{reg, {}}; + const auto base = AlignedWord(program.userData[index]); + for (std::size_t run = 0; run < variant.runs.size() && run < variant.runBases.size(); ++run) { + if (variant.runBases[run] != static_cast(index)) continue; + word.reads.emplace_back(static_cast(variant.runs[run].first - base), static_cast((variant.runs[run].second - variant.runs[run].first) / sizeof(std::uint32_t))); + } + shape->words.push_back(std::move(word)); + changed = true; + } + } + if (changed) { + std::sort(shape->words.begin(), shape->words.end(), [](const RelocatableUserWord& a, const RelocatableUserWord& b) { return a.reg < b.reg; }); + drawRelocations[pending.identity] = std::move(shape); + } + if (pending.alias == 0) return; + if (drawAliases.size() >= MaxAliases) drawRelocationCounters.evicted += dropEveryOther(drawAliases); + drawAliases[pending.alias] = inserted->second; +} + +void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr& entry, std::shared_ptr& decode, std::vector& patches) { + auto& counters = drawRelocationCounters; + ++counters.lookups; + const auto found = drawAliases.find(alias); + if (found == drawAliases.end() || found->second->decode == nullptr) return; + const auto donor = found->second; + const auto& programs = donor->decode->programs; + if (donor->stages.size() != programs.size()) return; + struct Move { + std::size_t program; + std::uint32_t index, from, to; + }; + std::vector moves; + for (std::size_t i = 0; i < programs.size(); ++i) { + const auto& program = programs[i]; + for (std::uint32_t index = program.hiddenUserWords; index < program.userData.size(); ++index) { + const auto current = readUserData(queue.shader, program.userDataBase + index - program.hiddenUserWords); + if (current != program.userData[index]) moves.push_back({i, index, program.userData[index], current}); + } + } + if (moves.empty()) return; + ++counters.donors; + auto relocated = std::make_shared(); + relocated->decode = donor->decode; + relocated->stages.resize(donor->stages.size()); + for (std::size_t i = 0; i < donor->stages.size(); ++i) { + const bool moved = std::any_of(moves.begin(), moves.end(), [&](const Move& move) { return move.program == i; }); + if (!moved) { + relocated->stages[i] = donor->stages[i]; + continue; + } + for (const auto& variant : donor->stages[i]) { + if (variant == nullptr || variant->compiled == nullptr) continue; + const bool allowed = std::all_of(moves.begin(), moves.end(), [&](const Move& move) { return move.program != i || (move.index < 64 && (variant->relocatableWords >> move.index & 1u) != 0 && move.from != 0 && move.to != 0 && ((move.from ^ move.to) & 3u) == 0); }); + if (!allowed) { + ++counters.refused; + continue; + } + auto copy = std::make_shared(); + copy->runs = variant->runs; + copy->runBases = variant->runBases; + copy->relocatableWords = variant->relocatableWords; + copy->words = variant->words; + copy->forgetSerial = variant->forgetSerial; + copy->compiled = variant->compiled; + copy->meshIndexDescriptor = variant->meshIndexDescriptor; + copy->shader = variant->shader; + copy->dataPositions = variant->dataPositions; + copy->dataSlots = variant->dataSlots; + copy->flatBinding = variant->flatBinding; + copy->pushOffset = variant->pushOffset; + copy->vertexInfo = variant->vertexInfo; + for (const auto& move : moves) { + if (move.program != i) continue; + const auto delta = AlignedWord(move.to) - AlignedWord(move.from); + for (std::size_t run = 0; run < copy->runs.size() && run < copy->runBases.size(); ++run) { + if (copy->runBases[run] != static_cast(move.index)) continue; + copy->runs[run].first += delta; + copy->runs[run].second += delta; + } + const auto& forwarded = variant->compiled->shaderDataUserWords; + for (std::uint32_t slot = 0; slot < forwarded.size(); ++slot) { + if (forwarded[slot] == move.index) copy->shaderDataPatches.emplace_back(slot, move.to); + } + } + relocated->stages[i].push_back(std::move(copy)); + } + } + patches.clear(); + for (const auto& move : moves) patches.push_back({move.program, move.index, move.to}); + entry = std::move(relocated); + decode = donor->decode; +} + +void Driver::applyShaderDataPatches(DispatchVariant& variant) { + if (variant.shaderDataPatches.empty()) return; + auto rebased = std::make_shared(*variant.compiled); + for (const auto& [slot, value] : variant.shaderDataPatches) { + const auto at = static_cast(slot) * sizeof(std::uint32_t); + if (at + sizeof(std::uint32_t) <= rebased->pushConstants.size()) std::memcpy(rebased->pushConstants.data() + at, &value, sizeof(value)); + for (auto& binding : rebased->bindings) { + if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData && slot < binding.guestDescriptor.size()) binding.guestDescriptor[slot] = value; + } + } + variant.compiled = std::move(rebased); + variant.shaderDataPatches.clear(); +} + +void Driver::reportDrawRelocation() { + auto& counters = drawRelocationCounters; + AgcDriver::ProfilePrint_nid_no_patch("[draw-relocate] %llu alias lookups: %llu donors with moved user words, %llu refusals, %llu hits through a relocated entry; %zu shader sets learned, %zu aliases held (%llu dropped at the limit)\n", static_cast(counters.lookups), static_cast(counters.donors), static_cast(counters.refused), static_cast(counters.hits), drawRelocations.size(), drawAliases.size(), static_cast(counters.evicted)); + counters = {}; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp index 21d38fdea8..e52db4829b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp @@ -31,7 +31,9 @@ std::optional Driver::ClassifyIndirectDraw(const Sha constexpr std::uint32_t NoLocation = 0x280u; const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList; const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation); - if ((graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) && !meshGpuSide) fail(Path::NonVertexPath); + static const bool rectListOnGpu = std::getenv("APS5_NO_RECT_INDIRECT") == nullptr; + const bool rectListGpuSide = rectListOnGpu && graphics.rectList && !graphics.stages.mesh && !graphics.stages.tessellation; + if ((graphics.stages.path != Graphics::ShaderPath::Vertex || (graphics.rectList && !rectListGpuSide)) && !meshGpuSide) fail(Path::NonVertexPath); if (indirect.drawIndexEnabled && indirect.drawIndexSgpr >= 0) fail(Path::DrawIndex); if (indirect.countIndirect && !support.count) fail(Path::FeatureGap); if (indirect.countIndirect && indirect.count > 1 && !support.multi) fail(Path::FeatureGap); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 5853975c5f..01693be19c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -99,6 +99,10 @@ extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::Fl return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info); } +extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() { + return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead(); +} + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { AgcDriver::RegisterVideoOutput(handle, output); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 8fdd044922..91c50d6f3f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -1,12 +1,18 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "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/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "ShaderDiskCache.hpp" #include #include #include @@ -38,6 +44,7 @@ void PrintFailure(std::exception_ptr error) { } catch (...) { std::fprintf(stderr, "[gpu] driver failure with a non-standard exception\n"); } + FlushAsyncLog_nid_no_patch(); std::fflush(stderr); } @@ -112,18 +119,25 @@ void Driver::stop() { for (auto& [queue, worker] : workers) { if (worker.thread.joinable()) worker.thread.join(); } + DrawPipeline::Queue0().Stop(); StopWorkerSampler(); CloseRegistrationPreparation(); + Graphics::StopPipelineLibraryWorker(); std::lock_guard gpuLock(GuestMemory::GpuMutex()); device.Reset(); replacedDevices.clear(); + Graphics::FlushPipelineJournal(); Graphics::ShutdownGuestBufferWorkers(); + Graphics::StopCompareHelpers(); + ShaderRecompiler::ShaderDiskCache::Shutdown(); PerformanceTrace::StopOutput(); + FlushAsyncLogAtEnd_nid_no_patch(); StopProfileOutput_nid_no_patch(); stopped = true; } Driver::Driver() { + InstallAsyncLog_nid_no_patch(); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { @@ -216,3 +230,11 @@ bool& Driver::onWorkerThread() { } } + +namespace AgcDriver { + +std::unique_lock HoldAsyncLog() { + return AsyncLog::Get().Hold(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index 34b051bb28..47b88455e4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -50,6 +50,31 @@ void Driver::noteWrittenBuffers(std::uint64_t program, std::uint32_t queue, cons while (writtenBuffers.size() > WrittenBufferRing) writtenBuffers.pop_front(); } +std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded) { + bounded = compiled.bdaAbiVersion == 0; + std::vector> ranges; + constexpr std::uint64_t UnknownImageBytes = 64ull * 1024 * 1024; + for (const auto& binding : compiled.bindings) { + if (binding.role == ShaderRecompiler::DescriptorRole::Gds || binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer) bounded = false; + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element]; + if (!written) continue; + if (binding.guestDescriptor.size() < (static_cast(element) + 1) * 8) { + bounded = false; + continue; + } + const auto resource = Graphics::DecodeTextureResource(std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8)); + if (resource.baseAddress == 0) continue; + const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; + ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes)); + if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes)); + } + } + visitWrittenBuffers(compiled, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); }); + return ranges; +} + void Driver::noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue) { if (!foreignWriters() || !(writeEvidenceEnabled() || traceCapSync()) || end <= begin) return; std::lock_guard lock(writtenBuffersMutex); 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 4bd279a45f..1c35594004 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -314,8 +314,10 @@ void Driver::execute(const Submission& submission) { currentPacketOffset() = cursor; if (gateOpened()) { gateOpened() = false; - if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); - timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); + if (!pipelineDispatch() || submission.queue != 0) { + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); + timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); + } } if (opcode == 0x15) timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); }); else timed(&WorkerProfile::dispatchMs, [&] { dispatchIndirect(queue, packet, submission); }); @@ -409,7 +411,10 @@ void Driver::execute(const Submission& submission) { return; } if ((std::atomic_ref(*const_cast(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) { - DrawPipeline::Queue0().Enqueue([this] { submitOpenWork(); }, {}); + DrawPipeline::Queue0().Enqueue([this] { + submitOpenWork(); + wakeQueueZeroForCompletions(); + }, {}); executeRewindTail(submission); return; } 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 e914fdd8bb..ed1cb37640 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" @@ -84,6 +85,12 @@ void Driver::submitOpenWork() { if (localDevice != nullptr) localDevice->SubmitRecorded(true); } +void Driver::wakeQueueZeroForCompletions() { + if (!completionsPending()) return; + std::lock_guard lock(mutex); + changed.notify_all(); +} + void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span labelWrites) { struct Complete { Driver& driver; @@ -97,7 +104,7 @@ void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, st driver.forgetUnfinishedWrites(driver.workers.at(0), labelWrites); const auto found = std::find(driver.queue0Uncommitted.begin(), driver.queue0Uncommitted.end(), received); if (found != driver.queue0Uncommitted.end()) driver.queue0Uncommitted.erase(found); - notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0; + notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0 || completionsPending(); } if (notify) driver.changed.notify_all(); } @@ -213,9 +220,9 @@ void Driver::run(std::uint32_t id) noexcept { && ((!storeIdleResults && Graphics::StorageTexture::AnyPending()) || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); - changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); }); + changed.wait(lock, [&] { return ready() || (id == 0 && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())); }); if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed); - if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue; + if (id == 0 && !ready() && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())) continue; break; } if (poll(ready)) break; @@ -233,10 +240,18 @@ void Driver::run(std::uint32_t id) noexcept { if (APS5_ENABLE_TIMING_LOG) submission.dequeuedAt = std::chrono::steady_clock::now(); if (id == 0) queue0Executing = submission.suspend ? 0 : submission.received; if (submission.waitFree) { - orderHolders.fetch_add(1, std::memory_order_acq_rel); - const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); }; - while (!released()) poll(released); - orderHolders.fetch_sub(1, std::memory_order_acq_rel); + { + struct OrderHold { + std::atomic& holders; + explicit OrderHold(std::atomic& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); } + ~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); } + OrderHold(const OrderHold&) = delete; + OrderHold& operator=(const OrderHold&) = delete; + } hold{orderHolders}; + const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); }; + StallWatch stall(id); + while (!released() && !poll(released)) stall.Check("order behind queue 0 for event", submission.received); + } rethrowFailure(); } if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index b7cfef3d62..b1572bfa26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -259,6 +260,8 @@ void Driver::reserveOutputs(Submission& submission) { void Driver::executeRewindTail(const Submission& stalled) { PerformanceTimer timing("Driver.Rewind"); + static const bool ring = ReadSubmissionRingSettings(); + if (ring) PrepareSubmissionRing(stalled.queue); std::atomic_ref control(*const_cast(stalled.rewindTail - 1)); if ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) { noteWaitBlocked(stalled.queue, reinterpret_cast(stalled.rewindTail - 1), true); @@ -267,10 +270,12 @@ void Driver::executeRewindTail(const Submission& stalled) { std::uint32_t queue; ~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); } } blocked{*this, stalled.queue}; + StallWatch stall(stalled.queue); while ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) { CheckFailure(); checkStopping(); noteAwaitingTitle(stalled.queue, reinterpret_cast(stalled.rewindTail - 1)); + stall.Check("REWIND at", reinterpret_cast(stalled.rewindTail - 1)); PollSleep(); } } @@ -303,6 +308,7 @@ void Driver::executeRewindTail(const Submission& stalled) { tail.serial = stalled.serial; tail.received = ++eventSerial; if (capture != nullptr) capture->Take(captured, tail.serial, captureTime(), tail.heldAtSubmit); + if (ring) RecordSubmission(tail.queue, tail.serial, tail.commands, stalled.rewindTail, true); } if (capture != nullptr) writeCaptured(*this, *capture, captured); if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now(); @@ -313,7 +319,9 @@ void Driver::executeRewindTail(const Submission& stalled) { void Driver::Submit(const Packet* packet, std::uint32_t queue) { const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; CheckFailure(); - require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue"); + require(queue == 0 || (queue >= 0x20 && queue < SubmissionQueueEnd), "unsupported compute queue"); + static const bool ring = ReadSubmissionRingSettings(); + if (ring) PrepareSubmissionRing(queue); GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet)); const auto descriptor = *packet; require(descriptor.flags == 0, "nonzero submission flags are not implemented"); @@ -355,6 +363,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { const auto now = std::chrono::steady_clock::now(); submission.enqueuedAt = now; if (capture != nullptr) capture->Take(captured, submission.serial, captureTime(), submission.heldAtSubmit); + if (ring) RecordSubmission(queue, submission.serial, submission.commands, descriptor.addr, false); if (profile) { ++costs.submissions; costs.validateNs += static_cast(std::chrono::duration_cast(validated - copied).count()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp new file mode 100644 index 0000000000..900cbd946d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp @@ -0,0 +1,281 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libc/include/FailureReport.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace AgcDriver::DriverDetail { + +namespace { + +constexpr std::size_t KeptWords = 512; +constexpr std::size_t MaximumCapacity = 4096; +constexpr std::size_t MaximumStallSeconds = 86400; +constexpr std::size_t PayloadWordsShown = 8; + +struct Entry { + std::uint64_t tenthsOfMs = 0; + std::uint64_t serial = 0; + std::uint64_t guest = 0; + std::size_t dwords = 0; + std::size_t kept = 0; + bool rewindTail = false; + std::array words{}; +}; + +struct Ring { + std::mutex mutex; + std::once_flag allocated; + std::atomic recorded{0}; + std::unique_ptr entries; +}; + +void reportFailure() noexcept; + +struct Rings { + std::array queues; +}; + +Rings& rings() { + static auto* value = new Rings; + return *value; +} + +struct Registration { + Registration() { LibcRegisterFailureReport_nid_no_patch(reportFailure); } + ~Registration() { LibcUnregisterFailureReport_nid_no_patch(reportFailure); } + Registration(const Registration&) = delete; + Registration& operator=(const Registration&) = delete; +}; + +std::size_t environmentCount(const char* name, std::size_t fallback, std::size_t maximum, const char* reason) { + const char* text = std::getenv(name); + if (text == nullptr) return fallback; + const auto* end = text + std::strlen(text); + std::size_t value = 0; + const auto [last, error] = std::from_chars(text, end, value); + require(error == std::errc{} && last == end && value <= maximum, reason); + return value; +} + +bool writeLine(const char* text, std::size_t length) { +#ifdef _WIN32 + const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE); + if (handle == nullptr || handle == INVALID_HANDLE_VALUE) return false; + DWORD written = 0; + return WriteFile(handle, text, static_cast(length), &written, nullptr) != 0 && written == length; +#else + while (length != 0) { + const auto written = ::write(2, text, length); + if (written < 0 && errno == EINTR) continue; + if (written <= 0) return false; + text += written; + length -= static_cast(written); + } + return true; +#endif +} + +class Line { +public: + void Add(const char* format, ...) { + if (length >= sizeof(text) - 2) return; + va_list arguments; + va_start(arguments, format); + const int added = std::vsnprintf(text + length, sizeof(text) - 1 - length, format, arguments); + va_end(arguments); + if (added > 0) length = std::min(length + static_cast(added), sizeof(text) - 2); + } + + bool Write() { + text[length] = '\n'; + const bool written = writeLine(text, length + 1); + length = 0; + return written; + } + +private: + char text[512]; + std::size_t length = 0; +}; + +void addName(Line& line, std::uint32_t header) { + const auto name = Pm4::KnownName(header); + if (name.empty()) line.Add("UNKNOWN_0x%02x", static_cast((header >> 8u) & 0xffu)); + else line.Add("%.*s", static_cast(name.size()), name.data()); +} + +bool writePackets(const Entry& entry, Line& line) { + bool written = true; + for (std::size_t cursor = 0; cursor < entry.kept;) { + const auto header = entry.words[cursor]; + line.Add("[pm4 ring] +0x%04zx ", cursor); + if (Pm4::FillerPacket(header)) { + std::size_t run = 1; + while (cursor + run < entry.kept && Pm4::FillerPacket(entry.words[cursor + run])) ++run; + addName(line, header); + line.Add(" x%zu", run); + written = line.Write() && written; + cursor += run; + continue; + } + if ((header >> 30u) != 3u) { + line.Add("not a type-3 header:"); + for (std::size_t i = 0; i <= PayloadWordsShown && cursor + i < entry.kept; ++i) line.Add(" %08x", entry.words[cursor + i]); + written = line.Write() && written; + break; + } + const auto words = Pm4::PacketWords(header); + const auto shown = std::min(words, PayloadWordsShown + 1); + addName(line, header); + for (std::size_t i = 1; i < shown && cursor + i < entry.kept; ++i) line.Add(" %08x", entry.words[cursor + i]); + if (cursor + shown > entry.kept) line.Add(" (cut)"); + else if (words > shown) line.Add(" (+%zu dwords)", words - shown); + written = line.Write() && written; + cursor += words; + } + return written; +} + +bool dump(std::string_view reason, bool failing) { + const auto capacity = SubmissionRingCapacity(); + const auto label = static_cast(std::min(reason.size(), 200)); + Line line; + if (capacity == 0) { + line.Add("[pm4 ring] %.*s: disabled (APS5_PM4_RING=0)", label, reason.data()); + return line.Write(); + } + auto& state = rings(); + std::size_t used = 0; + for (const auto& ring : state.queues) used += ring.recorded.load(std::memory_order_acquire) != 0 ? 1 : 0; + if (used == 0) { + line.Add("[pm4 ring] %.*s: no submissions recorded", label, reason.data()); + return line.Write(); + } + line.Add("[pm4 ring] %.*s: %zu queue(s), up to %zu submissions each, oldest first", label, reason.data(), used, capacity); + bool written = line.Write(); + for (std::size_t queue = 0; queue < state.queues.size(); ++queue) { + auto& ring = state.queues[queue]; + if (ring.recorded.load(std::memory_order_acquire) == 0) continue; + std::unique_lock lock(ring.mutex, std::defer_lock); + if (failing) { + for (int attempt = 0; attempt < 100 && !lock.try_lock(); ++attempt) std::this_thread::yield(); + } else { + lock.lock(); + } + const auto recorded = ring.recorded.load(std::memory_order_acquire); + if (!lock.owns_lock()) { + line.Add("[pm4 ring] queue 0x%zx: %llu submissions recorded, ring busy", queue, static_cast(recorded)); + written = line.Write() && written; + continue; + } + const auto first = recorded > capacity ? recorded - capacity : 0; + line.Add("[pm4 ring] queue 0x%zx: %llu submissions recorded, last %llu kept", queue, static_cast(recorded), static_cast(recorded - first)); + written = line.Write() && written; + for (auto ordinal = first; ordinal < recorded; ++ordinal) { + const auto& entry = ring.entries[ordinal % capacity]; + line.Add("[pm4 ring] queue 0x%zx #%llu serial %llu t=%llu.%llu ms: %zu dwords from 0x%llx%s, first %zu kept", queue, static_cast(ordinal + 1), static_cast(entry.serial), + static_cast(entry.tenthsOfMs / 10), static_cast(entry.tenthsOfMs % 10), + entry.dwords, static_cast(entry.guest), entry.rewindTail ? " (rewind tail)" : "", entry.kept); + written = line.Write() && written; + written = writePackets(entry, line) && written; + } + } + return written; +} + +void reportFailure() noexcept { + try { + dump("failure report", true); + } catch (...) { + static constexpr char text[] = "[pm4 ring] failure report: dump failed\n"; + writeLine(text, sizeof(text) - 1); + } +} + +} + +std::size_t SubmissionRingCapacity() { + static const std::size_t value = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "APS5_PM4_RING must be a number of submissions from 0 to 4096"); + return value; +} + +std::chrono::seconds StallWatchdogPeriod() { + static const std::chrono::seconds value(environmentCount("APS5_STALL_WATCHDOG_SECS", 0, MaximumStallSeconds, "APS5_STALL_WATCHDOG_SECS must be a number of seconds from 0 to 86400")); + return value; +} + +bool ReadSubmissionRingSettings() { + StallWatchdogPeriod(); + return SubmissionRingCapacity() != 0; +} + +void PrepareSubmissionRing(std::uint32_t queue) { + const auto capacity = SubmissionRingCapacity(); + require(capacity != 0, "the PM4 ring is disabled"); + require(queue < SubmissionQueueEnd, "PM4 ring queue out of range"); + static const Registration registration; + auto& ring = rings().queues[queue]; + std::call_once(ring.allocated, [&] { ring.entries = std::make_unique(capacity); }); +} + +void RecordSubmission(std::uint32_t queue, std::uint64_t serial, std::span commands, const void* guest, bool rewindTail) { + const auto capacity = SubmissionRingCapacity(); + require(capacity != 0, "the PM4 ring is disabled"); + require(queue < SubmissionQueueEnd, "PM4 ring queue out of range"); + auto& ring = rings().queues[queue]; + require(ring.entries != nullptr, "PM4 ring recorded before it was prepared"); + const auto tenthsOfMs = static_cast(std::max(TraceMs(), 0.0) * 10.0 + 0.5); + std::lock_guard lock(ring.mutex); + const auto recorded = ring.recorded.load(std::memory_order_relaxed); + auto& entry = ring.entries[recorded % capacity]; + entry.tenthsOfMs = tenthsOfMs; + entry.serial = serial; + entry.guest = reinterpret_cast(guest); + entry.dwords = commands.size(); + entry.kept = std::min(commands.size(), KeptWords); + entry.rewindTail = rewindTail; + std::copy_n(commands.begin(), entry.kept, entry.words.begin()); + ring.recorded.store(recorded + 1, std::memory_order_release); +} + +void DumpSubmissionRings(std::string_view reason) { + require(dump(reason, false), "cannot write the PM4 ring dump to stderr"); +} + +void StallWatch::Check(const char* waiter, std::uint64_t value) { + if (dumped) return; + const auto period = StallWatchdogPeriod(); + if (period.count() == 0) return; + const auto now = std::chrono::steady_clock::now(); + if (since == std::chrono::steady_clock::time_point{}) { + since = now; + return; + } + if (now - since < period) return; + dumped = true; + char reason[192]; + std::snprintf(reason, sizeof(reason), "queue 0x%x %s 0x%llx has not progressed for %lld s (APS5_STALL_WATCHDOG_SECS)", queue, waiter, static_cast(value), static_cast(period.count())); + DumpSubmissionRings(reason); +} + +} 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 b9a7b3ed9c..1b1299da12 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -11,6 +11,7 @@ #include "CompiledVariant.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include #include #include @@ -933,7 +934,8 @@ class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { static auto* pool = [] { - auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); + std::lock_guard lifecycle(Lifecycle()); + auto* created = new RegistrationPreparePool(Closing() ? 0u : std::max(2u, std::thread::hardware_concurrency() / 2u)); PreparationSlot().store(created); return created; }(); @@ -947,7 +949,8 @@ public: static RegistrationPreparePool& Prebuild() { static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); - auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); + std::lock_guard lifecycle(Lifecycle()); + auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); PrebuildSlot().store(created); return created; }(); @@ -958,7 +961,33 @@ public: return PrebuildSlot().load(); } + static unsigned LiveWorkers() { + return liveWorkers.load(std::memory_order_acquire); + } + + static void CloseAll() { + std::array pools{}; + { + std::lock_guard lifecycle(Lifecycle()); + Closing() = true; + pools = {Existing(), ExistingPrebuild()}; + } + for (auto* pool : pools) { + if (pool != nullptr) pool->Close(); + } + } + private: + static std::mutex& Lifecycle() { + static std::mutex lifecycle; + return lifecycle; + } + + static bool& Closing() { + static bool closing = false; + return closing; + } + static std::atomic& PreparationSlot() { static std::atomic slot{nullptr}; return slot; @@ -1002,9 +1031,17 @@ public: } void Close() { - std::unique_lock lock(mutex); - closed = true; - idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + std::vector joined; + { + std::unique_lock lock(mutex); + closed = true; + idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + joined.swap(workers); + } + ready.notify_all(); + for (const auto worker : joined) { + if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); + } } private: @@ -1015,8 +1052,9 @@ private: for (unsigned index = 0; index < count; ++index) { pthread_t thread; if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { - pthread_detach(thread); + workers.push_back(thread); ++threads; + liveWorkers.fetch_add(1, std::memory_order_acq_rel); } } pthread_attr_destroy(&attributes); @@ -1029,7 +1067,11 @@ private: Task task; { std::unique_lock lock(pool.mutex); - pool.ready.wait(lock, [&] { return !pool.tasks.empty(); }); + pool.ready.wait(lock, [&] { return !pool.tasks.empty() || pool.closed; }); + if (pool.tasks.empty()) { + liveWorkers.fetch_sub(1, std::memory_order_acq_rel); + return nullptr; + } task = std::move(pool.tasks.front()); pool.tasks.pop_front(); ++pool.running; @@ -1062,7 +1104,9 @@ private: std::condition_variable ready; std::condition_variable idle; std::deque tasks; + std::vector workers; unsigned threads = 0; + inline static std::atomic liveWorkers{0}; unsigned running = 0; std::uint64_t submitted = 0; std::set unfinished; @@ -1119,9 +1163,16 @@ void SubmitLibraryPrebuild(std::function task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } +void AwaitLibraryPrebuilds() { + if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); +} + void CloseRegistrationPreparation() { - RegistrationPreparePool::Instance().Close(); - if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); + RegistrationPreparePool::CloseAll(); +} + +unsigned RegistrationPreparationThreads() { + return RegistrationPreparePool::LiveWorkers(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { @@ -1462,6 +1513,11 @@ ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device) { return null; } +std::shared_ptr Driver::RegisteredShaders() { + std::lock_guard lock(mutex); + return shaders; +} + void Driver::RegisterShader(const Shader* shader) { PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.Register"); @@ -1514,7 +1570,11 @@ void Driver::RegisterShader(const Shader* shader) { registered.context = snapshot.registeredState->context; registered.userConfig = snapshot.registeredState->userConfig; std::shared_ptr preparation; - if (AsyncRegistrationPrepare()) { + static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; + if (lazy) { + static_cast(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred)); + snapshot.prepared->deferred = true; + } else if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); if (preparation != nullptr) preparation->prebuildDevice = localDevice; if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 3deebcb869..ef7a88bb2f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -330,6 +331,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que const auto spinStart = std::chrono::steady_clock::now(); bool spinning = pauseSpin; std::uint32_t polls = 0; + StallWatch stall(queue); while (!Pm4::WaitSatisfiedUnchecked(packet)) { if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { if (writer != queue) notePendingWait(packet, queue, received); @@ -358,7 +360,9 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (const auto done = packetsDone.load(); done != lastDone || (!requireMemory && packetsInFlight.load() != 0)) { lastDone = done; start = std::chrono::steady_clock::now(); + stall.Progressed(); } + stall.Check(((packet[0] >> 8u) & 0xffu) == 0x93u ? "WAIT_REG_MEM_64 at" : "WAIT_REG_MEM at", awaited); if (!warned && std::chrono::steady_clock::now() - start > std::chrono::milliseconds(waitTimeoutMs())) { warned = true; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab061..7c7379b6f1 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/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp" #include "ThreadOwned.hpp" @@ -1931,12 +1932,12 @@ void GpuMutexType::lock() { if (!GpuLockProfiled()) { auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr; if (trace == nullptr) { - mutex.lock(); + LockSpinning(mutex, HandoffSpinWindow()); } 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(); + LockSpinning(mutex, HandoffSpinWindow()); trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now()); } } @@ -1954,7 +1955,7 @@ void GpuMutexType::lock() { HolderMatrixValues before{}; if (holders) Holders().Snapshot(before); const auto start = std::chrono::steady_clock::now(); - mutex.lock(); + LockSpinning(mutex, HandoffSpinWindow()); 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); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index bdee46ee16..2a8ef54378 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -175,7 +175,7 @@ std::vector copyDataSource(std::span packet, con } -std::string Name(std::uint32_t header) { +std::string_view KnownName(std::uint32_t header) { if (FillerPacket(header)) return "PM4_FILLER"; const auto opcode = (header >> 8u) & 0xffu; if (opcode == 0x10 && (header & 0xfcu) != 0) { @@ -194,9 +194,14 @@ std::string Name(std::uint32_t header) { default: return "UNKNOWN_CUSTOM"; } } - for (const auto& entry : Opcodes) if (entry.value == opcode) return std::string(entry.name); + for (const auto& entry : Opcodes) if (entry.value == opcode) return entry.name; + return {}; +} + +std::string Name(std::uint32_t header) { + if (const auto known = KnownName(header); !known.empty()) return std::string(known); char text[24]{}; - std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", opcode); + std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", (header >> 8u) & 0xffu); return text; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 7dc7a4640c..3ff6cc3d86 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -31,6 +31,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* mesh = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT); const auto* clock = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR); const auto* drawParameters = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES); + const auto* terminate = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR); const auto* maintenance8 = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR); Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64"); @@ -46,6 +47,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta Require(!hasCapability(capability) || enabled, std::string("shader device profile advertises a disabled feature: ") + name); }; checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64"); + checkFeature(spv::CapabilityGeometry, core.geometryShader == VK_TRUE, "geometryShader"); checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader"); checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended"); checkFeature(spv::CapabilityClipDistance, core.shaderClipDistance == VK_TRUE, "shaderClipDistance"); @@ -76,6 +78,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta extensions.erase(std::unique(extensions.begin(), extensions.end()), extensions.end()); for (const auto& extension : extensions) extensionViews.emplace_back(extension); Require(std::ranges::find(extensionViews, "SPV_KHR_physical_storage_buffer") != extensionViews.end() && std::ranges::find(extensionViews, "SPV_KHR_8bit_storage") != extensionViews.end(), "shader device profile is missing runtime extensions"); + Require(std::ranges::find(extensionViews, "SPV_KHR_terminate_invocation") == extensionViews.end() || (terminate != nullptr && terminate->shaderTerminateInvocation == VK_TRUE), "shader device profile advertises a disabled feature: shaderTerminateInvocation"); this->target.supportedCapabilities = capabilities; this->target.supportedExtensions = extensionViews; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..bdef4030c0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,12 +1,15 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "BdaAbi.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" @@ -134,6 +137,7 @@ using ResourceCache = Graphics::ResourceCache; std::uint32_t DumpScale(); void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale); +FrameDumpOutput frameDumpOutput; class FrameDumpWriter { public: @@ -150,6 +154,7 @@ public: std::lock_guard lock(mutex); require(!stopping, "frame dump writer has stopped"); require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); + if (!frameDumpOutput.Enabled_nid_no_patch()) return; if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); pending.push_back({index, width, height, scale, std::move(pixels)}); @@ -212,6 +217,7 @@ struct ComputePipelineObjects { VkShaderModule module = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; + VkDescriptorSetLayout resourcesLayout = VK_NULL_HANDLE; ~ComputePipelineObjects() { if (pipeline != VK_NULL_HANDLE) destroyPipeline(device, pipeline, nullptr); if (layout != VK_NULL_HANDLE) destroyLayout(device, layout, nullptr); @@ -219,6 +225,16 @@ struct ComputePipelineObjects { } }; +// Pipelines are shared by dispatches of one module (the result's run-stable identity) with the +// same push constant use; descriptor set layouts built from the same bindings are compatible, so +// the pipeline layout of the first dispatch, or of the journal replay, serves them all. +struct ComputePipelineKey { + ShaderRecompiler::PipelineIdentity identity; + bool pushConstants = false; + auto operator<=>(const ComputePipelineKey&) const = default; + static ComputePipelineKey Of(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { return {shader.Identity(), pushConstants}; } +}; + constexpr std::uint64_t WorkgroupMemoryGranule = 1u << 20u; struct WorkgroupMemory { @@ -263,6 +279,7 @@ struct VulkanDevice::State { VkDeviceSize uploadSize = 0; VkPhysicalDeviceProperties properties{}; std::string driverInfo; + bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; bool deviceLostHook = false; @@ -274,8 +291,10 @@ struct VulkanDevice::State { bool tessellationShader = false; bool meshShader = false; bool fragmentShaderBarycentric = false; + bool shaderTerminateInvocation = false; bool geometryShader = false; bool sampleRateShading = false; + bool storageImageMultisample = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool shaderClock = false; @@ -337,6 +356,8 @@ struct VulkanDevice::State { bool computeWave32 = false; bool meshWave32 = false; std::uint32_t maxComputeSubgroupSize = 0; + bool computeFullSubgroups = false; + bool spannedWaves = false; // Indirect draw features enabled (see Graphics::Context). bool drawIndirectFirstInstance = false; bool multiDrawIndirect = false; @@ -359,8 +380,12 @@ struct VulkanDevice::State { // dispatch's find-or-insert and the verify switch's lookups touch the map, recipes hold weak // references to its objects. std::mutex computePipelinesMutex; - std::map> computePipelines; - std::map>> computePipelinesBuilding; + std::map> computePipelines; + std::map>> computePipelinesBuilding; + // The journal replay building this device's pipelines on its own threads; drained first at teardown. + std::shared_ptr journalReplay; + std::atomic journalReplayPending{false}; + std::atomic computePipelinesCreated{0}; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; std::unique_ptr emptyBuffer; @@ -668,6 +693,12 @@ struct VulkanDevice::State { } ~State() { + if (journalReplay != nullptr) { + journalReplay->Cancel(); + journalReplay->Wait(); + journalReplay.reset(); + Graphics::ReportPipelineJournal(); + } contextReady = false; if (deviceLostHook && Graphics::DeviceLostOwner() == this) { Graphics::DeviceLostHook() = nullptr; @@ -795,6 +826,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(state->physical, &properties); + require(capture.stageFlags == 0 || state->computeFullSubgroups, "captured full compute subgroups are unsupported"); require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported"); } const auto target = Target(); @@ -1098,6 +1130,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; + state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); @@ -1130,6 +1163,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->fragmentShaderBarycentric = FragmentShaderBarycentricUsable(driver.driverID, state->fragmentShaderBarycentric); } } + VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR terminateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR}; + if (hasExtension(VK_KHR_SHADER_TERMINATE_INVOCATION_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &terminateFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->shaderTerminateInvocation = terminateFeatures.shaderTerminateInvocation == VK_TRUE; + } VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT interlockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT}; if (hasExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &interlockFeatures}; @@ -1176,6 +1215,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric"); } + if (state->shaderTerminateInvocation) { + deviceExtensions.push_back(VK_KHR_SHADER_TERMINATE_INVOCATION_EXTENSION_NAME); + state->spirvExtensions.push_back("SPV_KHR_terminate_invocation"); + } if (state->fragmentShaderPixelInterlock) { deviceExtensions.push_back(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT); @@ -1263,7 +1306,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); - for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + for (const auto samples : {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { if ((locations.sampleLocationSampleCounts & samples) == 0) continue; VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; multisampleProperties(selected, samples, &multisample); @@ -1423,29 +1466,37 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : queueInfo.queueFamilyIndex = family; queueInfo.queueCount = 1; queueInfo.pQueuePriorities = &priority; -#ifdef _WIN32 const float priorities[2]{1.0f, 1.0f}; bool settleQueue = false; std::uint32_t settleFamily = family; - std::uint32_t families = 0; - auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); - getFamilies(selected, &families, nullptr); - std::vector queues(families); - getFamilies(selected, &families, queues.data()); - if (family < families && queues[family].queueCount >= 2) { - queueInfo.queueCount = 2; - queueInfo.pQueuePriorities = priorities; - settleQueue = true; - } else { - const auto copies = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; - for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { - if (copies(i) && (queues[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = i, settleQueue = true; - } - for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { - if (copies(i)) settleFamily = i, settleQueue = true; + std::uint32_t settleIndex = 0; + std::vector queues; +#ifdef _WIN32 + const bool settles = true; +#else + const bool settles = state->dmaBufImport; +#endif + if (settles) { + std::uint32_t families = 0; + auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); + getFamilies(selected, &families, nullptr); + queues.resize(families); + getFamilies(selected, &families, queues.data()); + if (family < families && queues[family].queueCount >= 2) { + queueInfo.queueCount = 2; + queueInfo.pQueuePriorities = priorities; + settleQueue = true; + settleIndex = 1; + } else { + const auto copies = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; + for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { + if (copies(i) && (queues[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = i, settleQueue = true; + } + for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { + if (copies(i)) settleFamily = i, settleQueue = true; + } } } -#endif VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; deviceInfo.queueCreateInfoCount = 1; deviceInfo.pQueueCreateInfos = &queueInfo; @@ -1479,7 +1530,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; - VkDeviceQueueCreateInfo queueInfos[2]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; + VkDeviceQueueCreateInfo queueInfos[3]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; if (available.sparseBinding && state->dmaBufImport) { std::uint32_t families = 0; auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); @@ -1507,19 +1558,22 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; } deviceInfo.pQueueCreateInfos = queueInfos; -#ifdef _WIN32 if (settleQueue && settleFamily != family) { - if (deviceInfo.queueCreateInfoCount == 1) { - queueInfos[1] = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; - queueInfos[1].queueFamilyIndex = settleFamily; - queueInfos[1].queueCount = 1; - queueInfos[1].pQueuePriorities = &priority; - deviceInfo.queueCreateInfoCount = 2; + if (deviceInfo.queueCreateInfoCount == 2 && queueInfos[1].queueFamilyIndex == settleFamily) { + settleQueue = queues[settleFamily].queueCount >= 2; + if (settleQueue) { + queueInfos[1].queueCount = 2; + queueInfos[1].pQueuePriorities = priorities; + settleIndex = 1; + } } else { - settleQueue = false; + auto& settleInfo = queueInfos[deviceInfo.queueCreateInfoCount++]; + settleInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + settleInfo.queueFamilyIndex = settleFamily; + settleInfo.queueCount = 1; + settleInfo.pQueuePriorities = &priority; } } -#endif // Indirect draw records with a non-zero first instance, and several records per call. enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance; enabled.multiDrawIndirect = available.multiDrawIndirect; @@ -1538,6 +1592,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; // Recompiled storage-image access declares no format (the guest descriptor decides it). enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample; + state->storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample); state->capabilities.push_back(spv::CapabilityImageMSArray); enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; @@ -1648,6 +1703,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : barycentricFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &barycentricFeatures; } + if (state->shaderTerminateInvocation) { + terminateFeatures.shaderTerminateInvocation = VK_TRUE; + terminateFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &terminateFeatures; + } if (state->fragmentShaderPixelInterlock) { interlockFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &interlockFeatures; @@ -1706,10 +1766,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->meshWave32 = state->meshShader && subgroupSizeFeatures.subgroupSizeControl == VK_TRUE && subgroupSize.minSubgroupSize <= 32u && subgroupSize.maxSubgroupSize >= 32u && (subgroupSize.requiredSubgroupSizeStages & VK_SHADER_STAGE_MESH_BIT_EXT) != 0; state->maxComputeSubgroupSize = subgroupSize.maxSubgroupSize; + state->computeFullSubgroups = state->computeWave32 && subgroupSizeFeatures.computeFullSubgroups == VK_TRUE; } if (state->computeWave32 || state->meshWave32) { subgroupSizeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT}; subgroupSizeFeatures.subgroupSizeControl = VK_TRUE; + subgroupSizeFeatures.computeFullSubgroups = state->computeFullSubgroups ? VK_TRUE : VK_FALSE; deviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME); deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); @@ -1717,6 +1779,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : byteFeatures.pNext = &subgroupSizeFeatures; APS5_LOG_OUT("Wave32 programs run on subgroups of %u: compute %d, mesh %d", 32u, state->computeWave32, state->meshWave32); } + if (std::getenv("APS5_SPANNED_WAVE") != nullptr) { + state->spannedWaves = state->computeFullSubgroups; + APS5_LOG_OUT("APS5_SPANNED_WAVE: single-wave wave64 compute spans two full 32-lane subgroups: %d", state->spannedWaves); + } timelineFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR}; timelineFeatures.timelineSemaphore = VK_TRUE; if (state->timelineSemaphores) { @@ -1804,12 +1870,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); state->queueFamily = family; -#ifdef _WIN32 if (settleQueue) { state->settleFamily = settleFamily; - state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleFamily == family ? 1 : 0, &state->settleQueue); + state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue); } -#endif if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue); if (checkpoints) state->checkpointMarker = state->DeviceFunction("vkCmdSetCheckpointNV"); if (deviceFault || checkpoints) { @@ -1839,7 +1903,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : Graphics::PrepareImportWatch(graphicsContext()); state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - state->pipelineCache = std::make_unique(graphicsContext(), state->properties); + state->pipelineCache = std::make_unique(graphicsContext(), state->properties, window != nullptr || compileOnly); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); state->colorTransfer = std::make_unique(graphicsContext()); @@ -1925,10 +1989,63 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, state->surfaceFormat); state->presentation = std::make_unique(graphicsContext(), state->presentSlots.size()); } + // A device without a window is usually replaced by the window's device before the title draws + // (Driver::AttachWindow): its replay starts at its first draw or dispatch instead, so the + // journal is replayed once, on the device the title draws with. + state->journalReplayPending.store(!compileOnly, std::memory_order_release); + if (window != nullptr) startJournalReplay(); +} + +void VulkanDevice::startJournalReplay() { + if (!state->journalReplayPending.exchange(false, std::memory_order_acq_rel)) return; + state->journalReplay = Graphics::StartPipelineJournalReplay(graphicsContext(), Graphics::PipelineJournalDeviceFingerprint(state->properties), {[this](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { return ReplayComputePipeline(shader, pushConstants); }}); } VulkanDevice::~VulkanDevice() = default; +VkPipelineCreateFlags VulkanDevice::computePipelineFlags() const { + return (graphicsContext().pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; +} + +std::uint32_t VulkanDevice::computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const { + return state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; +} + +VkPipelineShaderStageCreateFlags VulkanDevice::computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const { + if (!shader.fullSubgroups) return 0; + require(state->computeFullSubgroups && computeSubgroupSize(shader) == 32u, "a spanned wave64 program needs full 32-lane compute subgroups"); + return VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; +} + +bool VulkanDevice::ComputeFullSubgroups() const { + return state->computeFullSubgroups; +} + +void VulkanDevice::WaitForPipelineJournalReplay() { + startJournalReplay(); + if (state->journalReplay != nullptr) state->journalReplay->Wait(); +} + +std::uint64_t VulkanDevice::ComputePipelinesCreated() const { + return state->computePipelinesCreated.load(std::memory_order_relaxed); +} + +bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); + const auto key = ComputePipelineKey::Of(shader, pushConstants); + if (key.identity.Empty()) return false; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return false; + } + const auto context = graphicsContext(); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const auto layouts = Graphics::DescribeSetLayouts(context, shaders); + auto objects = createComputePipeline(context, shader, layouts.layouts.main, layouts.mainKey, pushConstants ? VK_SHADER_STAGE_COMPUTE_BIT : 0u, 0); + std::lock_guard pipelines(state->computePipelinesMutex); + return state->computePipelines.try_emplace(key, std::move(objects)).second; +} + bool VulkanDevice::ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (adoptDevice) GuestMemory::AssertGpuLockHeld("VulkanDevice::ImportGuestMemory"); return Graphics::ImportMappedRanges(graphicsContext(), ranges, generation, adoptDevice); @@ -1937,10 +2054,22 @@ bool VulkanDevice::ImportGuestMemory(const GuestAllocations::Mapped& ranges, std void VulkanDevice::PrepareForReplacement() { GuestMemory::AssertGpuLockHeld("VulkanDevice::PrepareForReplacement"); require(state->recorder != nullptr && Graphics::Recorder::Active() == state->recorder.get(), "device replacement requires its active recorder"); + state->journalReplayPending.store(false, std::memory_order_release); + if (state->journalReplay != nullptr) { + state->journalReplay->Cancel(); + state->journalReplay->Wait(); + state->journalReplay.reset(); + } + DriverDetail::AwaitLibraryPrebuilds(); Graphics::FlushCachedTextures(state->device); Graphics::PublishAllShadows(state->context, Graphics::PublishReason::Teardown); WaitIdle(); Graphics::DestroyShadows(state->device); + // The replacement's draws never find this device's pipelines (the store keys on the device), and + // the device lives on until the driver stops: destroy them now rather than then. + Graphics::ClearCachedPipelines(state->device); + std::lock_guard pipelines(state->computePipelinesMutex); + state->computePipelines.clear(); } void VulkanDevice::WaitIdle() { @@ -2719,20 +2848,10 @@ void VulkanDevice::PresentPixels(std::uint32_t width, std::uint32_t height, std: return value ? std::atoi(value) : 0; }(); static int dumped = 0; - if (dumped < dumpLimit) { + if (frameDumpOutput.Enabled_nid_no_patch() && dumped < dumpLimit) { char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", dumped++); - if (std::FILE* file = std::fopen(name, "wb")) { - const std::uint32_t imageBytes = width * height * 4; - const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; - const std::uint16_t magic = 0x4d42; - const std::uint32_t fileBytes = 54 + imageBytes; - std::fwrite(&magic, 2, 1, file); - std::fwrite(&fileBytes, 4, 1, file); - std::fwrite(header + 1, 4, 12, file); - std::fwrite(pixels.data(), 1, pixels.size(), file); - std::fclose(file); - } + frameDumpOutput.Write_nid_no_patch(name, width, height, pixels); } // Tests and tools present pixels synchronously; the game path splits the steps (see Driver::Present). if (!present(width, height, true, pixels)) return; @@ -2760,6 +2879,7 @@ std::uint32_t DumpScale() { // frame_.bmp: every DumpScale()-th pixel of a BGRA8 image, 32-bit top-down. void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale) { require(scale != 0 && full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); + if (!frameDumpOutput.Enabled_nid_no_patch()) return; const std::uint32_t width = (fullWidth + scale - 1) / scale; const std::uint32_t height = (fullHeight + scale - 1) / scale; std::vector pixels(static_cast(width) * height * 4); @@ -2768,20 +2888,7 @@ void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, } char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); - { - std::ofstream file; - file.exceptions(std::ios::failbit | std::ios::badbit); - file.open(name, std::ios::binary); - const std::uint32_t imageBytes = width * height * 4; - const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; - const std::uint16_t magic = 0x4d42; - const std::uint32_t fileBytes = 54 + imageBytes; - file.write(reinterpret_cast(&magic), 2); - file.write(reinterpret_cast(&fileBytes), 4); - file.write(reinterpret_cast(header + 1), 48); - file.write(reinterpret_cast(pixels.data()), static_cast(pixels.size())); - file.close(); - } + frameDumpOutput.Write_nid_no_patch(name, width, height, pixels); } ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) { @@ -2887,7 +2994,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (passthrough) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); const auto pixels = passthrough ? ReadDisplayBufferTexels(buffer) : ReadDisplayBuffer(buffer); bool dumpFrame = false; - if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (passthrough) { WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, ReadDisplayBuffer(buffer), DumpScale()); } else if (dumps.cpu) { @@ -2942,7 +3049,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; bool dumpFrame = false; - if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu || (passthrough && resident == nullptr)) { auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); @@ -3383,6 +3490,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.float16DenormPreserve = state->float16DenormPreserve; + target.spannedWaves = state->spannedWaves; target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.srgbDecodeFormats = state->srgbDecodeFormats; @@ -3465,6 +3573,7 @@ Graphics::Context VulkanDevice::buildContext() const { state->hostImportAlignment }; context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; + context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets; context.dmaBufImport = state->dmaBufImport; context.sparseBinding = state->sparseBinding; context.queueFamily = state->queueFamily; @@ -3507,8 +3616,10 @@ Graphics::Context VulkanDevice::buildContext() const { context.samplerTableCapacity = SamplerTableCapacity(context); context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; + context.storageImageMultisample = state->storageImageMultisample; context.shaderClipDistance = state->shaderClipDistance; context.shaderCullDistance = state->shaderCullDistance; + context.shaderTerminateInvocation = state->shaderTerminateInvocation; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); @@ -3523,6 +3634,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; + context.cacheReclaim = Graphics::ReleaseTextureCaches; return context; } @@ -3577,6 +3689,7 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage } void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { + startJournalReplay(); for (const auto& shader : shaders) { require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); @@ -3609,11 +3722,24 @@ bool TemplateDataRefresh() { return enabled; } +bool DispatchMove() { + static const bool enabled = std::getenv("APS5_NO_DISPATCH_MOVE") == nullptr && TemplateDataRefresh(); + return enabled; +} + +std::size_t DispatchMoveBytes() { + static const std::size_t bytes = [] { + const char* text = std::getenv("APS5_DISPMOVE_MAX_KIB"); + return static_cast(text != nullptr ? std::strtoull(text, nullptr, 10) : 1024ull) * 1024u; + }(); + return bytes; +} + // The content key of a compute stage, naming the device: the cache is process-wide and the driver // replaces the headless device with the windowed one while workers may still use the old one, so // the key names the device that built the entry (its descriptor set and pooled buffers belong to it). -ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device) { - auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh()); +ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device, bool movable = true) { + auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh(), movable && DispatchMove()); const auto deviceHandle = reinterpret_cast(device); key.push_back(static_cast(deviceHandle)); key.push_back(static_cast(deviceHandle >> 32u)); @@ -3644,6 +3770,7 @@ struct PreparedDispatch { std::shared_ptr resources; std::shared_ptr cached; ResourceCache::Key key; + bool exact = false; // APS5_PROFILE_DRAW: stage A's time, added to the [dispatch] phase totals by the dispatch, and // the prepare's parts for the driver's 'prepare:' rows (PreparePhase order). double prepareMs = 0; @@ -3676,6 +3803,8 @@ struct PresyncCounters { std::atomic checked{0}; std::atomic presyncs{0}; std::atomic waitedUs{0}; + std::atomic keyChecks{0}; + std::atomic keyWaits{0}; std::atomic lastReport{0}; }; @@ -3841,6 +3970,63 @@ RecipeCounters& Recipes() { return counters; } +constexpr std::size_t MoveRefusals = static_cast(Graphics::ShaderResources::MoveRefusal::Count); + +struct DispatchMoveCounters { + std::atomic checked{0}, exact{0}, moved{0}, buffers{0}, bytes{0}; + std::atomic rebuilt{0}, kept{0}, served{0}; + std::array, 5> sizes{}; + std::array, MoveRefusals> refused{}; + std::array, MoveRefusals> filtered{}; + std::atomic lastReport{0}; +}; + +DispatchMoveCounters& DispatchMoves() { + static DispatchMoveCounters counters; + return counters; +} + +void reportDispatchMove() { + auto& moves = DispatchMoves(); + const auto nowMs = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto last = moves.lastReport.load(); + if (nowMs - last < 10000 || !moves.lastReport.compare_exchange_strong(last, nowMs)) return; + const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; + const auto list = [&](std::array, MoveRefusals>& counters) { + std::string text; + unsigned long long total = 0; + char item[80]; + for (std::size_t i = 1; i < MoveRefusals; ++i) { + const auto count = take(counters[i]); + total += count; + if (count == 0) continue; + std::snprintf(item, sizeof(item), "%s %s %llu", text.empty() ? "" : ",", Graphics::ShaderResources::MoveRefusalName(static_cast(i)), count); + text += item; + } + std::snprintf(item, sizeof(item), "%llu (", total); + return std::string(item) + text + ")"; + }; + const auto checked = take(moves.checked); + const auto exact = take(moves.exact); + const auto moved = take(moves.moved); + const auto buffers = take(moves.buffers); + const auto bytes = take(moves.bytes); + const auto rebuilt = take(moves.rebuilt); + const auto kept = take(moves.kept); + const auto served = take(moves.served); + unsigned long long sizes[5]; + for (std::size_t i = 0; i < 5; ++i) sizes[i] = take(moves.sizes[i]); + const auto refused = list(moves.refused); + const auto filtered = list(moves.filtered); + AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s at stage A (the hit was dropped before the record), %s at record; after a refusal: %llu rebuilt, %llu inserted beside the movable template (kept), %llu served by an exact entry\n", checked, exact, moved, buffers, static_cast(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], filtered.c_str(), refused.c_str(), rebuilt, kept, served); +} + +void noteDispatchMoveSize(std::size_t bytes) { + auto& moves = DispatchMoves(); + const std::size_t bucket = bytes <= 1024 ? 0 : bytes <= 16384 ? 1 : bytes <= 262144 ? 2 : bytes <= 4194304 ? 3 : 4; + moves.sizes[bucket].fetch_add(1, std::memory_order_relaxed); +} + void reportRecipes() { auto& recipes = Recipes(); const auto nowMs = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); @@ -3991,12 +4177,23 @@ struct RecordedDispatch { // Whether the data refresh recorded anything. bool refreshed = false; bool resourceKeyMatched = false; + std::shared_ptr bindings{}; }; VkDevice VulkanDevice::Device() const { return state->device; } +VulkanDevice::DispatchMoveCount VulkanDevice::DispatchMoveCounts() { + auto& moves = DispatchMoves(); + DispatchMoveCount count{moves.checked.load(), moves.exact.load(), moves.moved.load(), 0, 0, moves.rebuilt.load(), moves.kept.load(), moves.served.load()}; + for (std::size_t i = 1; i < MoveRefusals; ++i) { + count.refused += moves.refused[i].load(); + count.filtered += moves.filtered[i].load(); + } + return count; +} + bool VulkanDevice::DispatchRecipes() { static const bool noDispatchRecipe = std::getenv("APS5_NO_DISPATCH_RECIPE") != nullptr; return !noDispatchRecipe; @@ -4058,23 +4255,31 @@ RecipeOutcome VulkanDevice::DrawFromRecipe(const Graphics::State& graphics, cons return RecipeOutcome::Recorded; } -std::uint64_t VulkanDevice::presync(std::span> surfaces) { +std::uint64_t VulkanDevice::presync(std::span surfaces) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; auto& counters = Presyncs(); bool overlaps = false; - for (const auto& [address, bytes] : surfaces) overlaps = overlaps || Graphics::Recorder::SnapshotWriteOverlaps(address, static_cast(bytes)); + for (const auto& surface : surfaces) { + const auto bytes = static_cast(surface.bytes); + if (surface.dccKeys) counters.keyChecks.fetch_add(1, std::memory_order_relaxed); + overlaps = overlaps || (surface.dccKeys ? Graphics::DccKeyWritersPending(surface.address, bytes) : Graphics::Recorder::SnapshotWriteOverlaps(surface.address, bytes)); + } std::uint64_t serial = 0; + bool texelsPending = false; if (overlaps) { // No reap here (SubmitAndEpoch's would run completions, which a compute worker must not do // on queue 0's behalf, see SubmitRecorded): the dispatch reaps after the wait. std::lock_guard lock(GuestMemory::GpuMutex()); if (state->recorder) { bool open = false; - for (const auto& [address, bytes] : surfaces) { - const auto info = state->recorder->DescribePendingWrite(address, static_cast(bytes)); + for (const auto& surface : surfaces) { + const auto bytes = static_cast(surface.bytes); + if (surface.dccKeys && !Graphics::DccKeyWriterNeedsSync(surface.address, bytes)) continue; + const auto info = state->recorder->DescribePendingWrite(surface.address, bytes); if (!info.has_value()) continue; open = open || info->open; serial = std::max(serial, info->serial); + texelsPending = texelsPending || !surface.dccKeys; } if (open) serial = state->recorder->SubmitAndEpoch(); } @@ -4083,16 +4288,25 @@ std::uint64_t VulkanDevice::presync(std::span(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), std::memory_order_relaxed); } if (profile) { const auto nowMs = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); auto last = counters.lastReport.load(); - if (nowMs - last >= 10000 && counters.lastReport.compare_exchange_strong(last, nowMs)) AgcDriver::ProfilePrint_nid_no_patch("[presync] %llu dispatches checked, %llu pre-syncs waited %.0f ms (cumulative)\n", static_cast(counters.checked.load()), static_cast(counters.presyncs.load()), counters.waitedUs.load() / 1000.0); + if (nowMs - last >= 10000 && counters.lastReport.compare_exchange_strong(last, nowMs)) { + const auto keyWaitCounts = Graphics::KeyWaitCounts(); + AgcDriver::ProfilePrint_nid_no_patch("[presync] %llu dispatches checked, %llu pre-syncs waited %.0f ms (cumulative); key ranges %llu checked, %llu pre-syncs that only the keys required; WaitForKeyWriters %llu calls: %llu without a pending writer, %llu known keys, %llu fills leaving uncompressed, %llu synced\n", static_cast(counters.checked.load()), static_cast(counters.presyncs.load()), counters.waitedUs.load() / 1000.0, static_cast(counters.keyChecks.load()), static_cast(counters.keyWaits.load()), static_cast(keyWaitCounts.calls), static_cast(keyWaitCounts.noPending), static_cast(keyWaitCounts.known), static_cast(keyWaitCounts.fillsUncompressed), static_cast(keyWaitCounts.synced)); + } } return serial; } +VulkanDevice::PresyncStatistics VulkanDevice::PresyncCounts() { + const auto& counters = Presyncs(); + return {counters.checked.load(std::memory_order_relaxed), counters.presyncs.load(std::memory_order_relaxed), counters.keyChecks.load(std::memory_order_relaxed), counters.keyWaits.load(std::memory_order_relaxed)}; +} + std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress) { ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); // APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split. @@ -4119,6 +4333,15 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco prepared->key = DispatchContentKey(compute, context.device); phase(PreparedDispatch::PrepareKey); cached = state->resourceCache.Find(prepared->key); + if (cached != nullptr && DispatchMove()) { + Graphics::ShaderResources::MoveProof proof; + if (!cached->MovedDispatchBuffers(compute, nullptr, DispatchMoveBytes(), proof).has_value()) { + DispatchMoves().filtered[static_cast(proof.refusal)].fetch_add(1, std::memory_order_relaxed); + prepared->key = DispatchContentKey(compute, context.device, false); + prepared->exact = true; + cached = state->resourceCache.Find(prepared->key); + } + } prepared->cached = cached; phase(PreparedDispatch::PrepareFind); } @@ -4316,6 +4539,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recorder.Keep(record.objects); recorder.Keep(record.resources); if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes()); + if (record.bindings != nullptr) { + recorder.Keep(record.bindings); + for (const auto& snapshot : record.bindings->snapshots) recorder.KeepBytes(snapshot.buffer.get(), snapshot.buffer->Bytes().size()); + } recordStep(PhaseRecordKeeps); if (record.dataRefresh != RecordedDispatch::DataRefresh::None) { // The template's data buffers take this dispatch's words: a transfer write the pre-dispatch @@ -4365,7 +4592,9 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { ++d.preBarriersRecorded; } context.Resolved(&Graphics::DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->pipeline); - resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout); + if (record.bindings != nullptr) Graphics::Require(record.bindings->allocation.set != VK_NULL_HANDLE, "dispatch overlay has no descriptor set"); + if (record.bindings != nullptr) context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, 0, 1, &record.bindings->allocation.set, 0, nullptr); + else resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout); if (const auto dwords = record.shader->program->workgroupMemoryDwords; dwords != 0) { auto memory = state->WorkgroupMemoryFor(context, record.x, record.y, record.z, dwords); context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, ShaderRecompiler::WorkgroupMemoryDescriptorSet, 1, &memory->set, 0, nullptr); @@ -4436,11 +4665,76 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordCompletion); } -std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { +// The module, pipeline layout and pipeline of a compute result over `setLayout` (the set layout +// its bindings describe): what a dispatch or its preparation builds at the first use of a module, +// and what the journal replay builds on its own threads before that use. +std::shared_ptr VulkanDevice::createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; + const auto start = std::chrono::steady_clock::now(); + auto objects = std::make_shared(); + objects->device = state->device; + objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = shader.spirv.data(); + check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + objects->resourcesLayout = setLayout; + const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; + layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); + const VkPipelineCreateFlags pipelineFlags = computePipelineFlags(); + const Graphics::PipelineSpecialization specialization(shader); + const auto subgroupSize = computeSubgroupSize(shader); + const auto stageFlags = computeStageFlags(shader); + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { + ComputePipelineCapture capture; + capture.spirv = shader.spirv.Copy(); + ComputeDescriptorSetLayout resourcesLayout; + const bool updateAfterBind = layoutKey.size() % 4u == 1u; + resourcesLayout.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + for (std::size_t index = 0; index + 4u <= layoutKey.size(); index += 4u) { + const auto type = static_cast(layoutKey[index + 1u]); + resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); + } + capture.sets.push_back(std::move(resourcesLayout)); + if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (pushStages != 0) capture.pushConstants.push_back(push); + for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); + capture.requiredSubgroupSize = subgroupSize; + capture.stageFlags = stageFlags; + capture.flags = pipelineFlags; + capture.deviceName = state->properties.deviceName; + capture.vendorId = state->properties.vendorID; + capture.deviceId = state->properties.deviceID; + capture.driverVersion = state->properties.driverVersion; + capture.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); + const auto path = capture.Write(directory); + std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); + } + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, stageFlags, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); + objects->destroyModule(objects->device, objects->module, nullptr); + objects->module = VK_NULL_HANDLE; + computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); + state->computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); + if (GuestMemory::GpuMutex().HeldByThisThread()) computePipelinesCreatedLocked.fetch_add(1, std::memory_order_relaxed); + Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); + return objects; +} + +std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { + const auto pipelineKey = ComputePipelineKey::Of(shader, pushStages != 0); std::promise> building; - if (pipelineKey != 0) { + if (!pipelineKey.identity.Empty()) { std::shared_future> other; { std::lock_guard pipelines(state->computePipelinesMutex); @@ -4451,7 +4745,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap if (other.valid()) return other.get(); } const auto finish = [&](std::shared_ptr built, std::exception_ptr error) { - if (pipelineKey == 0) return built; + if (pipelineKey.identity.Empty()) return built; { std::lock_guard pipelines(state->computePipelinesMutex); if (built != nullptr) built = state->computePipelines.try_emplace(pipelineKey, built).first->second; @@ -4463,60 +4757,8 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap }; std::shared_ptr objects; try { - const auto start = std::chrono::steady_clock::now(); - objects = std::make_shared(); - objects->device = state->device; - objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); - objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); - objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); - const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; - VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; - layoutInfo.pSetLayouts = setLayouts.data(); - layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; - layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; - check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; - const Graphics::PipelineSpecialization specialization(shader); - const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { - ComputePipelineCapture capture; - capture.spirv = shader.spirv.Copy(); - ComputeDescriptorSetLayout resourcesLayout; - const bool updateAfterBind = layoutKey.size() % 4u == 1u; - resourcesLayout.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - for (std::size_t index = 0; index + 4u <= layoutKey.size(); index += 4u) { - const auto type = static_cast(layoutKey[index + 1u]); - resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); - } - capture.sets.push_back(std::move(resourcesLayout)); - if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); - if (pushStages != 0) capture.pushConstants.push_back(push); - for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); - capture.requiredSubgroupSize = subgroupSize; - capture.flags = pipelineFlags; - capture.deviceName = state->properties.deviceName; - capture.vendorId = state->properties.vendorID; - capture.deviceId = state->properties.deviceID; - capture.driverVersion = state->properties.driverVersion; - capture.apiVersion = state->properties.apiVersion; - std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); - const auto path = capture.Write(directory); - std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); - } - if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); - objects->destroyModule(objects->device, objects->module, nullptr); - objects->module = VK_NULL_HANDLE; - computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); - if (GuestMemory::GpuMutex().HeldByThisThread()) computePipelinesCreatedLocked.fetch_add(1, std::memory_order_relaxed); - Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); - if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); + objects = createComputePipeline(context, shader, setLayout, layoutKey, pushStages, programAddress); + if (Graphics::PipelineJournalEnabled()) Graphics::RecordComputePipeline(shader, pushStages != 0); } catch (...) { finish(nullptr, std::current_exception()); throw; @@ -4525,6 +4767,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap } VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { + startJournalReplay(); state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.Dispatch"); @@ -4544,11 +4787,9 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } auto pushBytes = Graphics::AssemblePushConstants(shaders); const auto context = graphicsContext(); - // Pipelines are shared by dispatches of one compiled variant; descriptor set layouts built from the - // same bindings are compatible, so the pipeline layout of the first dispatch serves them all. - const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; + const auto pipelineKey = ComputePipelineKey::Of(shader, pushStages != 0); std::shared_ptr objects = prepared != nullptr ? prepared->objects : nullptr; - if (objects == nullptr && pipelineKey != 0) { + if (objects == nullptr && !pipelineKey.identity.Empty()) { std::lock_guard pipelines(state->computePipelinesMutex); if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) objects = found->second; } @@ -4566,7 +4807,9 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // The object came from the resource cache: a compute template whose data buffers take this // dispatch's words at the record (ShaderResources::RefreshData). bool fromCache = false; + bool refusedMove = false; double revalidateMs = 0; + std::shared_ptr overlay; // The batches the pre-sync (PrepareDispatch) waited for are reaped first: their completions // (CPU write-backs) land before stage B or a cached object's Revalidate reads, and the flush // hook then finds nothing pending over their surfaces. @@ -4586,6 +4829,11 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // The image lookups of this call by outcome (stage B, a Revalidate or a locked build), for the // [indirect] line: reset here, read after the resources phase. if (profile && arguments != 0) Graphics::ThreadLookupOutcomes() = {}; + const auto noteRefusalBuild = [](bool inserted) { + auto& moves = DispatchMoves(); + moves.rebuilt.fetch_add(1, std::memory_order_relaxed); + if (inserted) moves.kept.fetch_add(1, std::memory_order_relaxed); + }; // The resource cache insert evicts the oldest entry, destroying its ShaderResources here when the // cache was the last owner: timed apart from the build. const auto insert = [&] { @@ -4634,6 +4882,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } if (cacheable) { ++d.cacheMisses; + if (prepared->exact) noteRefusalBuild(resources->Reusable()); if (resources->Reusable()) insert(); } } else if (cacheable) { @@ -4641,13 +4890,54 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // object it found: revalidated as a map hit would be (an entry replaced meanwhile is left // alone by the pointer-conditional Remove). APS5_NO_PREPARED_FIND=1 looks the key up again. static const bool preparedFind = std::getenv("APS5_NO_PREPARED_FIND") == nullptr; + const bool exactEntry = prepared != nullptr && prepared->exact; contentKey = prepared != nullptr && !prepared->key.empty() ? std::move(prepared->key) : DispatchContentKey(shaders[0], context.device); auto cached = preparedFind && prepared != nullptr && prepared->cached != nullptr ? std::move(prepared->cached) : state->resourceCache.Find(contentKey); if (cached != nullptr) { if (cached->Revalidate(shaders[0])) { - resources = std::move(cached); - fromCache = true; - ++d.cacheHits; + bool usable = true; + if (DispatchMove() && !exactEntry) { + auto& moves = DispatchMoves(); + Graphics::ShaderResources::MoveProof proof; + auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof); + moves.checked.fetch_add(1, std::memory_order_relaxed); + if (moved.has_value() && !moved->empty()) { + overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); + if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset(); + if (overlay == nullptr) { + proof.refusal = Graphics::ShaderResources::MoveRefusal::Bindings; + moved.reset(); + } + } + if (!moved.has_value()) { + usable = false; + moves.refused[static_cast(proof.refusal)].fetch_add(1, std::memory_order_relaxed); + } else if (moved->empty()) { + moves.exact.fetch_add(1, std::memory_order_relaxed); + } else { + moves.moved.fetch_add(1, std::memory_order_relaxed); + moves.buffers.fetch_add(proof.buffers, std::memory_order_relaxed); + moves.bytes.fetch_add(proof.bytes, std::memory_order_relaxed); + noteDispatchMoveSize(proof.bytes); + } + } + if (!usable) { + contentKey = DispatchContentKey(shaders[0], context.device, false); + refusedMove = true; + cached = state->resourceCache.Find(contentKey); + if (cached != nullptr && cached->Revalidate(shaders[0])) usable = true; + else if (cached != nullptr) { + state->resourceCache.Remove(contentKey, cached.get()); + ++d.cacheInvalidated; + state->recorder->Keep(std::move(cached)); + } + } + if (usable) { + if (refusedMove || exactEntry) DispatchMoves().served.fetch_add(1, std::memory_order_relaxed); + resources = std::move(cached); + fromCache = true; + ++d.cacheHits; + } } else { state->resourceCache.Remove(contentKey, cached.get()); ++d.cacheInvalidated; @@ -4666,6 +4956,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (profile) timer.add(PhaseResourcesFullBuild, std::chrono::duration(std::chrono::steady_clock::now() - buildStart).count()); if (cacheable) { ++d.cacheMisses; + if (refusedMove) noteRefusalBuild(resources->Reusable()); if (resources->Reusable()) insert(); } } @@ -4688,10 +4979,17 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec AgcDriver::ProfilePrint_nid_no_patch("[rescache] %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped), static_cast(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast(computePipelinesCreatedLocked.load(std::memory_order_relaxed))); reportRecipes(); + reportDispatchMove(); } timing.Mark("shader_resources"); timer.phase(PhaseResources); + static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr; + if (verifySetLayouts && objects != nullptr) require(objects->resourcesLayout == resources->Layout(), "the resident compute pipeline's set layout differs from the dispatch's"); if (objects == nullptr) { + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = Graphics::DescribeSetLayouts(context, shaders); + require(described.layouts.main == resources->Layout() && !described.layouts.split, "the set layout described from the compute shader differs from the dispatch's"); + } const auto layouts = resources->SetLayouts(); objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); timing.Mark("pipeline_create"); @@ -4703,6 +5001,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec IndirectOutcome outcome{0, 0}; RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; record.resourceKeyMatched = fromCache; + record.bindings = overlay; if (arguments != 0) decideIndirect(record, outcome, groupsText); // The indirect hold total below still covers the whole call of a CPU-resolved one. const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; @@ -4756,7 +5055,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // object the resource cache serves under this content key (reusable: no lease, no copied // writes, every direct region import- or mirror-served), so a hit's proof is the template's // Revalidate and nothing needs completion work. - if (recipeOut != nullptr && DispatchRecipes() && cacheable && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) { + if (recipeOut != nullptr && DispatchRecipes() && cacheable && overlay == nullptr && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) { auto recipe = std::make_shared(); recipe->device = state->device; recipe->templateRef = resources; @@ -4825,7 +5124,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu return RecipeOutcome::Rebuild; } std::shared_ptr mapped; - const auto pipelineKey = (shader.PipelineVariantId() << 1u) | (recipe.pushes ? 1u : 0u); + const auto pipelineKey = ComputePipelineKey::Of(shader, recipe.pushes); { std::lock_guard pipelines(state->computePipelinesMutex); if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) mapped = found->second; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index de95acd54e..3101ae4f90 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -54,7 +54,8 @@ struct SlabSlot { class BufferPool { public: explicit BufferPool(const Context& context); - static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory); + static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported = nullptr); + static VkDeviceSize ReclaimTarget(VkDeviceSize bytes); ~BufferPool(); BufferPool(const BufferPool&) = delete; BufferPool& operator=(const BufferPool&) = delete; @@ -116,6 +117,7 @@ private: // when shared; the device tier for device-local memory while it has a budget). Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties); static bool DeviceTierEnabled(); + static VkDeviceSize deviceBudget(const Context& context); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); VkDeviceSize destroy(const BufferAllocation& allocation) noexcept; @@ -127,6 +129,7 @@ private: // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes); VkDevice device; PFN_vkUnmapMemory unmap; PFN_vkDestroyBuffer destroyBuffer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index cba5321c23..8eaada9e84 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -171,6 +171,7 @@ struct Context { std::uint32_t settleFamily = 0; std::mutex* settleQueueMutex = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes) = nullptr; RenderCache* renderCache = nullptr; DrawQueue* drawQueue = nullptr; GraphicsPipelineCache* graphicsPipelines = nullptr; @@ -210,8 +211,10 @@ struct Context { bool variableDescriptorCount = false; std::uint32_t imageTableCapacity = 0; std::uint32_t samplerTableCapacity = 0; + bool individuallyFreeDescriptorSets = false; bool geometryShader = false; bool sampleRateShading = false; + bool storageImageMultisample = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool nullDescriptors = false; @@ -232,6 +235,7 @@ struct Context { VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; + bool shaderTerminateInvocation = false; PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const { return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; @@ -317,6 +321,7 @@ inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { } std::uint32_t ImageTableCapacity(const Context& context); +std::uint32_t StorageTableCapacity(const Context& context); std::uint32_t SamplerTableCapacity(const Context& context); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index d2e9dff3a6..765d2acde7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -99,6 +99,16 @@ DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceByt void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span bytes); void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys); std::optional WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes); +bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count); +bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count); +struct DccKeyWaitCounts { + std::uint64_t calls; + std::uint64_t noPending; + std::uint64_t known; + std::uint64_t fillsUncompressed; + std::uint64_t synced; +}; +DccKeyWaitCounts KeyWaitCounts(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 2f54bee302..40e1076b79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -26,6 +26,7 @@ void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const Dep void ClearDepthSurfaces(VkDevice device); void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes); bool DepthSurfaceAt(std::uint64_t address); +bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes); 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); std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index d852d67432..0b596093b7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -31,7 +31,7 @@ struct DrawInputCopy { DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uint64_t address, std::size_t bytes, std::size_t alignment, Recorder::SnapshotUse use); DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes); void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived); -const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment); +const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite = nullptr); struct InPlaceIndices { std::optional highest; bool reused = false; @@ -93,6 +93,16 @@ bool DrawRecipes(); // `rewritten` counts a GPU-side draw whose records were copied and patched with a constant. enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, Count }; void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false); +std::array(IndirectDrawPath::Count)> IndirectDrawCounts(); + +struct DrawPassCounts { + std::uint64_t draws = 0; + std::uint64_t passesBegun = 0; + std::uint64_t passesContinued = 0; + std::uint64_t keyStoreFlushes = 0; + std::uint64_t storeFlushes = 0; +}; +std::array DrawPassTotals(); const char* IndirectDrawPathName(IndirectDrawPath path); // Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 9efb6aac24..b8aad61006 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -66,11 +66,14 @@ void SetImportWatch(const Context& context, ImportWatch watch); // (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in // the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); void SetHostImportBudget(std::uint64_t bytes); std::uint64_t HostImportLiveBytes(); +// Tests: how many imports settleImport has used on the settle queue. +std::uint64_t HostImportsSettled(); bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Whether an existing import covers [address, address + bytes), without reconciling the imports // with the registry or making one (HostImportFor may take a registry lease): a hint for choices @@ -374,7 +377,7 @@ private: void completeSpaceWrites() const; // The region containing `address`: this build's own regions (sorted after UploadPrepare) or // the space's base regions, or null. - const Region* owner(std::uint64_t address) const; + const Region* owner(std::uint64_t address, std::uint64_t end) const; // Builds this build's regions from a lease as the per-build path always did: `base` gets the // ranges served in place (imports, mirrors), `copied` the ones copied per build. struct CopiedRange { @@ -429,6 +432,7 @@ private: // Whether `regions` is in ascending address order (true right after AcquireRegistered, whose // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; + bool regionsOverlap = false; std::vector> writes; std::vector> readsOverBase; std::vector>> heapReferences; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 237fd10ad0..98d65e8c03 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -49,11 +50,15 @@ private: }; std::uint64_t MultisampledColorBytes(const ColorTarget& target); +std::size_t MultisampledDccBytes(const ColorTarget& target); +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource); +bool MultisampleTargetStorable(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp index a996ab230b..483b2f45cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp @@ -16,6 +16,8 @@ struct CompareSpan { void CompareSpans(std::span spans, std::span equal); void CompareSpansFrom(std::span spans, std::span equal, std::size_t parallelBytes); unsigned CompareHelpers(); +void StopCompareHelpers(); +unsigned CompareHelpersRunning(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index a195283b06..6bd3a8dd82 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -94,14 +94,29 @@ private: std::vector framebuffers; }; -// The pipeline for this configuration, shared across draws and frames: keyed by the stages' variant -// ids (the rect-list tessellation pair by the vertex and fragment ids that generate it), the vertex -// input layout, the descriptor set layout key, push constant stages and every pipeline state field. -// A configuration whose stage has no variant id gets a private pipeline. Entries are LRU-bounded and -// only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per -// draw as before. +// The pipeline for this configuration, shared across draws and frames: keyed by the stages' +// identities (the rect-list tessellation pair and the geometry stage by the vertex and fragment +// results that generate them), the vertex input layout, the descriptor set layout key, push constant +// stages and every pipeline state field. A configuration whose stage has no module gets a private +// pipeline. A miss builds the pipeline outside the store's mutex (a concurrent build of the same +// key keeps the first inserted) and records its recipe in the pipeline journal. Entries are +// LRU-bounded (APS5_PIPELINE_STORE_ENTRIES, 1024) and only evicted once no recorded draw holds +// them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); -std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +// `created` reports whether this call built the pipeline (false: served from the store or private). +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr); +// Cumulative pipeline store outcomes since start-up (the [pipecache] line reports 10 s windows). +struct PipelineStoreCounters { + std::uint64_t hits = 0; + std::uint64_t misses = 0; + std::uint64_t uncached = 0; + std::uint64_t evicted = 0; + std::uint64_t cached = 0; + std::uint64_t bound = 0; +}; +PipelineStoreCounters PipelineStoreTotals(); +// The pipeline store's key of a configuration (empty when the configuration gets a private pipeline). +void BuildPipelineStoreKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout); struct DescribedSetLayouts { PipelineSetLayouts layouts; std::vector mainKey; @@ -126,7 +141,7 @@ bool ShadersWriteMemory(std::span shaders); bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program); void ValidateSampleMask(const Context& context, const State& state, 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, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = 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, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false, bool shaderTerminateInvocation = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 92506617e4..bff387e234 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -18,13 +18,14 @@ class PipelineCache { public: static constexpr int SaveIntervalSeconds = 10; - PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties); + PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile = true); ~PipelineCache(); PipelineCache(const PipelineCache&) = delete; PipelineCache& operator=(const PipelineCache&) = delete; VkPipelineCache Handle() const { return cache; } PipelineCacheSynchronization& Synchronization() { return synchronization; } + void SaveNow(); private: struct FileHeader { @@ -54,6 +55,7 @@ private: VkPipelineCache cache = VK_NULL_HANDLE; PipelineCacheSynchronization synchronization; std::filesystem::path path; + bool holdsFile = false; std::size_t loadedBytes = 0; std::size_t settledBytes = 0; std::optional settledFile; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp new file mode 100644 index 0000000000..c2cf90476e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp @@ -0,0 +1,104 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEJOURNAL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEJOURNAL_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +// What a graphics pipeline is built from besides the device: the stage results (named by their +// run-stable identity), the vertex input layout, the descriptor set layout keys and the state +// fields the Pipeline constructor reads, with every other State field cleared +// (CanonicalPipelineState). The pipeline store keys on exactly this, and the journal records it so +// the next run builds the same VkPipeline before the title asks for it. A generated stage (the +// rect-list control and evaluation pair, the barycentric geometry stage) is built from the vertex +// and fragment results and the device's limits; its identity is in the key like any other stage's, +// so a replayed pipeline built from another device's generated module is missed, not used. +struct GraphicsPipelineRecipe { + struct Stage { + ShaderRecompiler::ShaderStage stage; + ShaderRecompiler::PipelineIdentity identity; + std::uint32_t pushConstantOffset; + bool generated; + }; + VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + std::vector stages; + VertexInputLayout vertexInput; + std::vector mainLayoutKey; + std::vector fragmentLayoutKey; + bool splitSets = false; + State state; +}; + +[[nodiscard]] State CanonicalPipelineState(const State& state); +[[nodiscard]] bool GeneratedPipelineStage(const State& state, ShaderRecompiler::ShaderStage stage); +[[nodiscard]] GraphicsPipelineRecipe MakeGraphicsPipelineRecipe(const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout); +[[nodiscard]] std::vector EncodeGraphicsPipelineRecipe(const GraphicsPipelineRecipe& recipe); +[[nodiscard]] bool DecodeGraphicsPipelineRecipe(std::span bytes, GraphicsPipelineRecipe& recipe); + +struct PipelineJournalCounters { + std::uint64_t loadedRecipes = 0; + std::uint64_t loadedModules = 0; + std::uint64_t loadedBytes = 0; + std::uint64_t droppedRecords = 0; + double loadMs = 0; + std::uint64_t replayedGraphics = 0; + std::uint64_t replayedCompute = 0; + std::uint64_t replayPresent = 0; + std::uint64_t replaySkipped = 0; + std::uint64_t replayRejected = 0; + std::uint64_t replayFailed = 0; + double replayMs = 0; + std::uint32_t replayThreads = 0; + std::uint64_t recordedGraphics = 0; + std::uint64_t recordedCompute = 0; + std::uint64_t recordedModules = 0; + std::uint64_t writtenBytes = 0; + std::uint64_t writeFailures = 0; +}; + +// A decimal switch of the pipeline store or journal, or its default when unset; anything else throws. +[[nodiscard]] std::uint64_t PipelineSwitch(const char* name, std::uint64_t fallback); +[[nodiscard]] bool PipelineJournalEnabled(); +// True on a replay thread while it builds an entry: what it builds is already in the file. +[[nodiscard]] bool PipelineJournalReplaying(); +[[nodiscard]] std::filesystem::path PipelineJournalPath(); +[[nodiscard]] PipelineJournalCounters PipelineJournalTotals(); +void RecordGraphicsPipeline(const GraphicsPipelineRecipe& recipe, std::span shaders); +void RecordComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); +void FlushPipelineJournal(); +void ReportPipelineJournal(); + +// Cancel stops the threads taking further entries (the pipeline each is building completes); +// Wait joins them. A device cancels its replay before it is replaced or destroyed. +class PipelineJournalReplay { +public: + virtual ~PipelineJournalReplay() = default; + virtual void Cancel() = 0; + virtual void Wait() = 0; + [[nodiscard]] virtual bool Done() const = 0; +}; + +struct PipelineJournalHooks { + std::function compute; +}; + +// The journal belongs to the device that wrote it, named by the same properties as the driver's +// pipeline cache file; a replay for another device starts the file over. +[[nodiscard]] std::uint64_t PipelineJournalDeviceFingerprint(const VkPhysicalDeviceProperties& properties); +[[nodiscard]] std::shared_ptr StartPipelineJournalReplay(const Context& context, std::uint64_t deviceFingerprint, PipelineJournalHooks hooks); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index b2ca74e468..fcd6104569 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include #include +#include #include #include #include @@ -32,7 +33,13 @@ struct OptimizedPipeline { PFN_vkDestroyPipeline destroy = nullptr; std::atomic handle{VK_NULL_HANDLE}; std::atomic released{false}; + // The relink of a pipeline the journal replay built waits for the first draw that binds it + // (Schedule), so the relink thread serves the title's own pipelines first and a replayed + // pipeline the title never draws is never relinked. + std::atomic deferred{false}; + std::function relink; void Release() noexcept; + void Schedule(); }; VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false, std::span modules = {}); @@ -41,6 +48,9 @@ void WaitForOptimizedPipelines(VkDevice device); void ClearPipelineLibraries(VkDevice device); +void StopPipelineLibraryWorker(); +unsigned PipelineLibraryWorkers(); + struct PipelineLibraryCounters { std::array built{}; std::uint64_t linked = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 2470ab5323..30e547aaa6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -752,6 +752,8 @@ private: bool syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int source, const void* site); // `source` is the CountSync source the wait is attributed to. void finish(std::unique_ptr batch, bool wait, int source, bool releaseNow = false); + void discard(std::unique_ptr batch) noexcept; + void removeLabels(Batch& batch); void release(Batch& batch) noexcept; static bool overlaps(const Batch& batch, std::uint64_t address, std::uint64_t end); static std::optional readOverlap(const Batch& batch, std::uint64_t address, std::uint64_t end); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp index 162a2bd327..674cff0c47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp @@ -64,6 +64,7 @@ private: VkBufferUsageFlags usage; std::shared_ptr cache; bool hostPlaced = false; + bool ready = false; }; // Records a whole-range buffer copy. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f..b64df636f7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,10 +28,12 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); -std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); +std::uint64_t StorageTextureCacheBudget(const Context& context); struct TextureCacheUse { std::size_t sampledEntries = 0; std::uint64_t sampledBytes = 0; @@ -42,6 +44,7 @@ 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); bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); +bool SpirvStoresThroughAddresses(std::span words); // 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. @@ -98,6 +101,7 @@ private: PFN_vkDestroyDescriptorSetLayout destroyLayout; PFN_vkDestroyDescriptorPool destroyPool; PFN_vkResetDescriptorPool resetPool; + PFN_vkFreeDescriptorSets freeDescriptorSets = nullptr; mutable std::mutex mutex; std::map, VkDescriptorSetLayout> layouts; struct Pool { @@ -122,6 +126,12 @@ struct ImageTableReuseCounts { }; ImageTableReuseCounts ImageTableReuses(); +struct PresyncSurface { + std::uint64_t address; + std::uint64_t bytes; + bool dccKeys; +}; + class ShaderResources { public: ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes); @@ -168,8 +178,16 @@ public: std::size_t size; std::vector words; }; - std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}) const; + std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}, bool movedOnly = false) const; std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; + enum class MoveRefusal : std::size_t { None, Shape, NotGuest, Written, Limit, Inaccessible, Overlap, Bindings, Count }; + struct MoveProof { + MoveRefusal refusal = MoveRefusal::None; + std::size_t buffers = 0; + std::size_t bytes = 0; + }; + static const char* MoveRefusalName(MoveRefusal refusal); + std::optional> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const; static std::size_t MaxDrawSnapshotBytes(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU @@ -182,6 +200,7 @@ public: // Whether a written buffer was copied (its results reach guest memory by the CPU write-back). bool HasCopiedWrites() const { return guestMemory.HasCopiedWrites(); } bool HoldsLease() const { return guestMemory.HoldsLease(); } + bool BdaReadOnly() const { return usesBda && bdaReadOnly; } 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); } @@ -226,6 +245,7 @@ public: bool Reusable() const { return reusable; } const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); + static std::uint64_t DepthGuardRefusals(); const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; @@ -280,7 +300,7 @@ public: // Usable right after the deferred constructor, for address-based builds too (it decodes the // compiled shader's bindings itself when stage A has not run), and on a completed object from // the resource cache (its Revalidate repeats the same lookups), without the device lock. - std::vector> PresyncSurfaces() const; + std::vector PresyncSurfaces() const; private: friend class AgcDriver::VulkanDevice; @@ -338,12 +358,15 @@ private: std::uint32_t set = 0; std::size_t firstSampler = 0; std::size_t samplerCount = 0; + bool sampled = false; + bool storage = false; std::vector elementTextures; std::vector> samplerRejections; }; std::vector tableStages; std::array tableBindings{std::numeric_limits::max(), std::numeric_limits::max()}; std::array tableElements{}; + std::array storageTableBindings{std::numeric_limits::max(), std::numeric_limits::max()}; TableStage& tableStage(const CompiledShader& shader); static const ShaderRecompiler::DescriptorBinding* tableImages(std::span shaders, ShaderRecompiler::ShaderStage stage); std::uint32_t rejectTableElement(TableStage& table, std::span words, const std::string& message); @@ -375,6 +398,7 @@ private: // same way but is left out of the write set (no write-back, no pending-write note). `atomic` // is its bufferAtomic (see GuestBufferMemory::AddWritable). std::size_t addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read); + std::uint64_t guestBufferBytes(std::span words) const; std::size_t addDataBuffer(std::span words); // Stage A: the layout entry of an image binding (samplers are taken at once, the sampler cache // locks itself); stage B looks the sampled textures and storage images up (resolveImageBinding). @@ -486,6 +510,7 @@ private: std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; bool usesBda = false; + bool bdaReadOnly = true; std::vector multisampleSources; bool usesFaultBuffer = false; bool faultChecks = false; @@ -530,6 +555,7 @@ private: std::vector textureBaseLevel; std::vector textureSrgb; std::vector> storageTextures; + std::vector multisampleStorageViews; std::vector storageMips; std::vector storageKeys; std::vector storageFirstLayer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index b9844764d0..c2a2c0510b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -67,6 +67,7 @@ struct ColorTarget { bool uintExport = false; std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; + std::uint32_t dccControl = 0; }; struct DepthTarget { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index bc6efaf720..5a70e2f069 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -34,6 +34,7 @@ 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); +VkFormat HostStorageFormat(const Context& context, VkFormat format); bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); bool ClearKeepsDenormals(const Context& context, VkFormat format); @@ -185,6 +186,7 @@ public: void MarkDirty(); void MarkSliceDirty(std::uint32_t slice); void Flush(); + bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. // Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards. // Then the unit shadows over the range are published into their imports as `scope` says (see diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 1387258786..5c71753057 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -224,8 +224,6 @@ private: std::unordered_map entries; }; -// The whole byte range a vertex buffer descriptor covers (records * stride, or records when the -// stride is 0): what an indirect draw, whose counts only the GPU knows, copies for the fetch. inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::VertexAttribute& attribute) { return (attribute.resource.fields[3] >> 28u) & 3u; } @@ -247,7 +245,7 @@ inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& a const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; const auto records = attribute.resource.fields[2]; Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); - const auto bytes = stride == 0 ? ZeroStrideAvailableBytes(attribute) : static_cast(records) * stride; + const auto bytes = stride == 0 ? ZeroStrideAvailableBytes(attribute) : records == 0 ? 0 : static_cast(records - 1u) * stride + DecodeVertexFormat(attribute).bytes; Require(bytes != 0 && bytes <= std::numeric_limits::max(), "empty or oversized vertex buffer descriptor"); const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); Require(address != 0 && bytes <= std::numeric_limits::max() - address, "invalid vertex buffer address range"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 6ec77770be..7beb86bef3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -36,10 +36,10 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | } -BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) { +BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim) { smallTier.budget = smallBudget; largeTier.budget = budget; - deviceTier.budget = DeviceTierEnabled() ? DeviceBudget(context.memory) : 0; + deviceTier.budget = deviceBudget(context); } BufferPool::~BufferPool() { @@ -57,7 +57,9 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation const auto initialResult = result; unsigned attempts = 1; VkDeviceSize reclaimed = 0; - if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) { + VkDeviceSize released = 0; + const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; }; + if (exhausted(result)) { reclaimed = trim(); if (reclaimed != 0) { memory = VK_NULL_HANDLE; @@ -65,16 +67,28 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation ++attempts; } } + if (exhausted(result)) { + if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); + if (released != 0) { + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++attempts; + } + } if (result == VK_SUCCESS) { - if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(initialResult), attempts); + if (attempts > 1) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu released=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); return memory; } char details[512]; - std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(initialResult), attempts); + std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); Check(result, details); return VK_NULL_HANDLE; } +VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { + return bytes + std::max(bytes, VkDeviceSize{256} << 20u); +} + bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false; const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity); @@ -106,7 +120,11 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if (!slab.available.empty()) return takeFrom(*slab.available.back(), slab); } const auto blockBytes = SlabBlockBytes(capacity); - auto block = std::make_unique(); + const auto release = [this](SlabBlock* block) noexcept { + if (block->memory != VK_NULL_HANDLE) freeBlock(*block); + delete block; + }; + std::unique_ptr block(new SlabBlock, release); block->slotBytes = capacity; block->slab = key; VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; @@ -118,7 +136,6 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { void* mapping = nullptr; const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping); - if (mapped != VK_SUCCESS) freeMemory(device, block->memory, nullptr); Check(mapped, "vkMapMemory buffer slab"); block->mapping = static_cast(mapping); } @@ -127,8 +144,10 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, for (std::uint32_t index = slots; index-- != 0;) block->free.push_back(index); std::lock_guard lock(slabMutex); auto& slab = slabs[key]; + if (slab.available.size() == slab.available.capacity()) slab.available.reserve(std::max(1, slab.available.capacity() * 2)); auto& inserted = *block; - slabBlocks.emplace(inserted.memory, std::move(block)); + const auto owner = slabBlocks.try_emplace(inserted.memory).first; + owner->second.reset(block.release()); slab.available.push_back(&inserted); ++slab.emptyBlocks; return takeFrom(*slab.available.back(), slab); @@ -175,18 +194,27 @@ bool BufferPool::DeviceTierEnabled() { return enabled; } -VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory) { +VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { static const char* value = std::getenv("APS5_STAGING_POOL_MIB"); if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; - VkDeviceSize largest = 0; - bool systemMemory = false; + std::optional largest; for (std::uint32_t heap = 0; heap < memory.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { - if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 || memory.memoryHeaps[heap].size <= largest) continue; - largest = memory.memoryHeaps[heap].size; - systemMemory = systemMemoryHeap(memory, heap); + if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!largest || memory.memoryHeaps[heap].size > memory.memoryHeaps[*largest].size)) largest = heap; } - if (systemMemory) return std::max(VkDeviceSize{128} << 20u, largest / 64u); - return std::max(VkDeviceSize{512} << 20u, largest / 8u); + VkDeviceSize size = largest ? memory.memoryHeaps[*largest].size : 0; + if (largest && reported != nullptr && reported->heapBudget[*largest] != 0) size = std::min(size, reported->heapBudget[*largest]); + if (largest && systemMemoryHeap(memory, *largest)) return std::max(VkDeviceSize{128} << 20u, size / 64u); + return std::max(VkDeviceSize{512} << 20u, size / 8u); +} + +VkDeviceSize BufferPool::deviceBudget(const Context& context) { + if (!DeviceTierEnabled()) return 0; + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return DeviceBudget(context.memory); + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + context.memoryProperties2(context.physical, &properties); + return DeviceBudget(context.memory, &reported); } VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { @@ -223,7 +251,7 @@ VkBufferUsageFlags BufferPool::Usage(VkBufferUsageFlags usage, VkMemoryPropertyF } BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlags properties) { - if (DeviceTiered(properties)) return deviceTier; + if (DeviceTiered(properties) && (sharedTiers() || capacity >= classLimit)) return deviceTier; return !sharedTiers() && capacity < classLimit ? smallTier : largeTier; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index f8f4d371ec..98449f5291 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -761,13 +761,62 @@ bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureR } +namespace { + +struct KeyWaitTotals { + std::atomic calls{0}; + std::atomic noPending{0}; + std::atomic known{0}; + std::atomic fillsUncompressed{0}; + std::atomic synced{0}; +}; + +KeyWaitTotals& KeyWaits() { + static KeyWaitTotals totals; + return totals; +} + +} + +DccKeyWaitCounts KeyWaitCounts() { + const auto& totals = KeyWaits(); + return {totals.calls.load(std::memory_order_relaxed), totals.noPending.load(std::memory_order_relaxed), totals.known.load(std::memory_order_relaxed), totals.fillsUncompressed.load(std::memory_order_relaxed), totals.synced.load(std::memory_order_relaxed)}; +} + +bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count) { + if (metaAddress == 0 || count == 0 || !Recorder::SnapshotWriteOverlaps(metaAddress, count)) return false; + auto& memo = Memo(); + std::lock_guard lock(memo.mutex); + MemoRecorder(memo); + return std::none_of(memo.entries.begin(), memo.entries.end(), [&](const GpuKeyStore& store) { return store.note != 0 && store.begin <= metaAddress && metaAddress + count <= store.end; }); +} + +bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count) { + if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return false; + auto* recorder = Recorder::Active(); + return recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count) && !PendingStoreKeys(*recorder, metaAddress, count).has_value(); +} + std::optional WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes) { const auto count = static_cast(guestBytes / KeyBytes); const auto metaAddress = resource.dccAddress; if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt; + auto& waits = KeyWaits(); + waits.calls.fetch_add(1, std::memory_order_relaxed); auto* recorder = Recorder::Active(); - if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count) || PendingStoreKeys(*recorder, metaAddress, count).has_value()) return std::nullopt; - if (PendingFillsLeaveUncompressed(*recorder, resource, count)) return DccKeys::Uncompressed; + if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count)) { + waits.noPending.fetch_add(1, std::memory_order_relaxed); + return std::nullopt; + } + if (PendingStoreKeys(*recorder, metaAddress, count).has_value()) { + waits.known.fetch_add(1, std::memory_order_relaxed); + return std::nullopt; + } + if (PendingFillsLeaveUncompressed(*recorder, resource, count)) { + waits.fillsUncompressed.fetch_add(1, std::memory_order_relaxed); + return DccKeys::Uncompressed; + } + waits.synced.fetch_add(1, std::memory_order_relaxed); Recorder::CountSync(2); recorder->SyncThrough(metaAddress, count); return std::nullopt; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 59f21fea59..4f506c934a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -257,7 +257,9 @@ public: const VkImageSubresourceRange range{aspects, 0, 1, 0, 1}; constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_WRITE_BIT); - const VkClearDepthStencilValue clear{clearDepth, clearStencil}; + const VkClearDepthStencilValue clear{depthFill.value_or(clearDepth), stencilFill.value_or(clearStencil)}; + depthFill.reset(); + stencilFill.reset(); context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access); if (batch) batch->SubmitAndWait(); @@ -267,16 +269,20 @@ public: } } + bool UnormStencilView(const GuestTextureResource& resource) const { + return target.samples == 1 && target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress && ResolveTextureFormat(resource.format) == VK_FORMAT_R8_UNORM; + } + bool SampledAccepts(std::span words, const GuestTextureResource& resource) const { 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); const bool oneSlice = target.samples != 1 ? resource.dimension == TextureDimension::k2DMsaa : resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); - return (ResolveTextureFormat(resource.format) == expected || depthBits) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0; + return (ResolveTextureFormat(resource.format) == expected || depthBits || UnormStencilView(resource)) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0; } - std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components, bool drawn = false) { CheckCurrent(); CheckResource(resource); std::array key{}; @@ -298,6 +304,9 @@ public: Require(resource.samples == target.samples, "the sampled depth descriptor's sample count differs from its native target"); Require(hostFormat == target.format, "sampling a native multisampled depth plane that the device stores in another depth format is not implemented"); } + if (stencil && UnormStencilView(resource) && SampledAccepts(words, resource)) { + return unormStencil(key, components, resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, drawn); + } if (!SampledAccepts(words, resource)) { 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)", @@ -315,8 +324,25 @@ public: return texture; } + void ForgetClear(VkImageAspectFlags aspects) { + pendingClear &= ~aspects; + if ((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) depthFill.reset(); + if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) stencilFill.reset(); + } + + void NoteHtileClear(VkImageAspectFlags aspects) { + ForgetClear(aspects); + pendingClear |= aspects; + } + + void NotePlaneFill(VkImageAspectFlagBits aspect, float depth, std::uint8_t stencil) { + pendingClear |= aspect; + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) depthFill = depth; + else stencilFill = stencil; + } + void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { - pendingClear &= ~static_cast(aspect); + ForgetClear(aspect); auto* recorder = RecorderFor(context); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -334,7 +360,8 @@ public: } bool Holds(const Texture* texture) const { - return std::any_of(textures.begin(), textures.end(), [&](const auto& entry) { return entry.second.get() == texture; }); + const auto held = [&](const auto& entry) { return entry.second.get() == texture; }; + return std::any_of(textures.begin(), textures.end(), held) || std::any_of(stencilCopy.textures.begin(), stencilCopy.textures.end(), held); } std::uint64_t written = ++depthWrites; @@ -362,6 +389,8 @@ public: VkImageAspectFlags pendingClear = 0; float clearDepth = 0.0f; std::uint8_t clearStencil = 0; + std::optional depthFill; + std::optional stencilFill; bool StorageViewMatches(std::span words, const GuestTextureResource& resource) const { if (resource.width != target.extent.width || resource.height != target.extent.height) return false; @@ -372,7 +401,7 @@ public: return ResolveTextureFormat(resource.format) == expected || depthBits; } - void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const { + void RequireStorable(const GuestTextureResource& resource) const { const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented"); if (!stencil && hostFormat != target.format) { @@ -380,11 +409,6 @@ public: 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); - } } std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { @@ -396,7 +420,7 @@ public: std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); throw std::runtime_error(text); } - RequireStorable(resource, mip); + RequireStorable(resource); 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) { @@ -481,11 +505,101 @@ private: std::vector> replaced; std::unique_ptr staging; }; + struct StencilCopy { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::unique_ptr staging; + std::map, std::shared_ptr> textures; + std::uint64_t written = 0; + bool filled = false; + }; std::map, std::shared_ptr> textures; PlaneStorage depthPlane; PlaneStorage stencilPlane; std::uint64_t depthMemory = 0; std::uint64_t stencilMemory = 0; + StencilCopy stencilCopy; + + std::shared_ptr unormStencil(const std::array& key, VkComponentMapping components, VkImageViewType viewType, bool drawn) { + if (stencilCopy.image == VK_NULL_HANDLE) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = VK_FORMAT_R8_UNORM; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + 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, &stencilCopy.image), "vkCreateImage stencil copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, stencilCopy.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, &stencilCopy.memory), "vkAllocateMemory stencil copy"); + Check(context.Function("vkBindImageMemory")(context.device, stencilCopy.image, stencilCopy.memory, 0), "vkBindImageMemory stencil copy"); + stencilCopy.staging = std::make_unique(context, static_cast(target.extent.width) * target.extent.height, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + } catch (...) { + releaseStencilCopy(); + throw; + } + } + if (drawn || !stencilCopy.filled || stencilCopy.written != written) { + copyStencil(); + stencilCopy.written = drawn ? 0 : written; + } + auto& texture = stencilCopy.textures[key]; + if (texture == nullptr) { + texture = std::make_shared(context, stencilCopy.image, VK_FORMAT_R8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType); + texture->MarkRefreshedPerUse(); + } + return texture; + } + + void copyStencil() { + 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_WRITE_BIT); + VkBufferImageCopy region{0, 0, 0, {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_GENERAL, stencilCopy.staging->Handle(), 1, ®ion); + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransfer.oldLayout = stencilCopy.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 = stencilCopy.image; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkBufferMemoryBarrier staged{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, stencilCopy.staging->Handle(), 0, VK_WHOLE_SIZE}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &staged, 1, &toTransfer); + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + context.Function("vkCmdCopyBufferToImage")(commands, stencilCopy.staging->Handle(), stencilCopy.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + 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); + stencilCopy.filled = true; + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + } + + void releaseStencilCopy() noexcept { + stencilCopy.textures.clear(); + stencilCopy.staging.reset(); + if (stencilCopy.image) context.Function("vkDestroyImage")(context.device, stencilCopy.image, nullptr); + if (stencilCopy.memory) context.Function("vkFreeMemory")(context.device, stencilCopy.memory, nullptr); + stencilCopy = {}; + } std::uint64_t depthBytes() const { const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; @@ -532,6 +646,7 @@ private: void release() noexcept { depthPlane = {}; stencilPlane = {}; + releaseStencilCopy(); textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (owned != nullptr) owned.reset(); @@ -857,8 +972,13 @@ void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address auto& entry = owners[{surface.context.device, address}]; auto* owner = entry.surface; if (owner != nullptr && owner->active->load(std::memory_order_acquire) && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) { - if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect; - else surface.CopyAspectFrom(*owner, aspect); + if ((owner->pendingClear & aspect) != 0) { + surface.pendingClear |= aspect; + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) surface.depthFill = owner->depthFill; + else surface.stencilFill = owner->stencilFill; + } else { + surface.CopyAspectFrom(*owner, aspect); + } } entry = {&surface, ++planeBinds}; } @@ -1040,7 +1160,7 @@ void RetireUnmappedDepth_nid_no_patch(std::uintptr_t address, std::size_t bytes) } bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples) { - if (samples <= 1 || (context.sampleLocationSampleCounts & samples) == 0) return false; + if (samples == 0 || (context.sampleLocationSampleCounts & samples) == 0) return false; VkImageFormatProperties properties{}; const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, &properties); @@ -1128,9 +1248,9 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (created && !inherits && target.htileAddress == 0) surface->TakeStorage(); if (cleared && (!created || inherits)) { if (target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; - surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); } - surface->pendingClear |= htileCleared; + surface->NoteHtileClear(htileCleared); surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; surface->NoteMemory(); @@ -1196,7 +1316,7 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz } if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; if (surface->target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; - surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); existing = true; } if (!existing) clearedHtiles()[htileAddress] = bytes; @@ -1244,7 +1364,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanSampled(words, resource, components); } - if (resource.width != base.extent.width || resource.height != base.extent.height) return nullptr; + if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); @@ -1260,7 +1380,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanApplyFastClear(); - if (sampled) return found->Sampled(words, resource, components); + if (sampled) return found->Sampled(words, resource, components, found == drawnDepth); 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 layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; if (!planes) { @@ -1324,6 +1444,26 @@ bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t addre return address + bytes - htile >= HtileMetadataBytes(extent); } +bool PlaneFillCovers(std::uint64_t plane, std::uint64_t planeBytes, std::uint64_t address, std::size_t bytes) { + return plane != 0 && plane >= address && plane + planeBytes <= address + bytes; +} + +std::optional StencilFillValue(std::uint32_t pattern) { + const auto byte = static_cast(pattern & 0xffu); + if (pattern != byte * 0x01010101u) return std::nullopt; + return byte; +} + +std::optional DepthFillValue(std::uint32_t pattern, bool d16) { + if (d16) { + if ((pattern >> 16u) != (pattern & 0xffffu)) return std::nullopt; + return static_cast(pattern & 0xffffu) / 65535.0f; + } + const auto value = std::bit_cast(pattern); + if (!(value >= 0.0f && value <= 1.0f)) return std::nullopt; + return value; +} + void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(surfacesMutex()); forgetHtileReads(address, bytes); @@ -1331,9 +1471,20 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 if (!surface->active->load(std::memory_order_acquire)) continue; const auto& target = surface->target; if (target.samples != 1u && target.htileAddress != 0u && address < target.htileAddress + HtileMetadataBytes(target.extent) && target.htileAddress < address + bytes) surface->sampling.refusal = "native multisample depth metadata fill transition is not implemented"; - if (!HtileFillCovers(target.htileAddress, target.extent, address, bytes)) continue; - const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); - surface->pendingClear = (surface->pendingClear & ~written) | HtileFillClears(pattern, target.htileStencil); + if (HtileFillCovers(target.htileAddress, target.extent, address, bytes)) { + const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + surface->ForgetClear(written); + surface->NoteHtileClear(HtileFillClears(pattern, target.htileStencil)); + continue; + } + if (target.samples != 1) continue; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + if (PlaneFillCovers(target.address, DepthSliceBytes(target.extent, d16 ? 2u : 4u), address, bytes)) { + if (const auto depth = DepthFillValue(pattern, d16)) surface->NotePlaneFill(VK_IMAGE_ASPECT_DEPTH_BIT, *depth, 0); + } + if (PlaneFillCovers(target.stencilAddress, DepthSliceBytes(target.extent, 1u), address, bytes)) { + if (const auto stencil = StencilFillValue(pattern)) surface->NotePlaneFill(VK_IMAGE_ASPECT_STENCIL_BIT, 0.0f, *stencil); + } } } @@ -1369,7 +1520,7 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept const auto layers = resource.depthOrLastArray + 1u; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytesPerTexel = d16 ? 2u : 4u; - surface.RequireStorable(resource, mip); + surface.RequireStorable(resource); const auto geometry = DescribeSurface(resource); if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { char text[256]; @@ -1388,6 +1539,7 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept } std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip) { + if (mip != 0 || resource.baseLevel != 0) return nullptr; std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { @@ -1424,6 +1576,11 @@ bool DepthSurfaceAt(std::uint64_t address) { return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && (surface->target.address == address || surface->target.stencilAddress == address); }); } +bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes) { + std::lock_guard lock(surfacesMutex()); + return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && surface->context.device == device && surface->Overlaps(address, bytes); }); +} + void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes) { std::lock_guard lock(surfacesMutex()); for (const auto& surface : surfaces()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e..d23793371f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -353,6 +353,9 @@ struct DrawOutcome { // A recorded draw began a render pass of its own, or continued the previous draw's. bool passBegun = false; bool passContinued = false; + bool keyStoresFlushed = false; + bool storesFlushed = false; + bool addressDraw = false; SyncReason reason = SyncNone; // Shader validation memo (see CachedFragmentOutputs). bool validateMemoized = false; @@ -425,6 +428,7 @@ struct DrawProfile { // Render passes recorded draws began, and draws that continued the previous draw's pass. std::uint64_t passesBegun = 0; std::uint64_t passesContinued = 0; + std::array passTotals{}; std::array reasons{}; // The CPU inside Graphics::Draw, split by outcome: a synchronous draw costs ten times a recorded // one, so one average would only show the synchronous population. @@ -495,6 +499,14 @@ void reportDraw(const std::array& us, const ShaderResources: if (outcome.completion) ++profile.completion; if (outcome.passBegun) ++profile.passesBegun; if (outcome.passContinued) ++profile.passesContinued; + if (outcome.recorded) { + auto& totals = profile.passTotals[outcome.addressDraw ? 1 : 0]; + ++totals.draws; + if (outcome.passBegun) ++totals.passesBegun; + if (outcome.passContinued) ++totals.passesContinued; + if (outcome.keyStoresFlushed) ++totals.keyStoreFlushes; + if (outcome.storesFlushed) ++totals.storeFlushes; + } ++profile.reasons[outcome.reason]; (outcome.recorded ? (outcome.waited ? profile.waitedUs : profile.recordedUs) : profile.synchronousUs) += drawUs; profile.targetLookups += outcome.targetLookups; @@ -646,6 +658,7 @@ bool ValidationKey(const Context& context, std::span shade add(context.shaderClipDistance); add(context.shaderCullDistance); add(context.viewportIndexLayer); + add(context.shaderTerminateInvocation); add(state.paClVsOutCntl); add(state.stages.mesh.has_value()); if (state.stages.mesh) { @@ -658,6 +671,7 @@ bool ValidationKey(const Context& context, std::span shade add(mesh.threadsPerGroup); add(mesh.ldsSizeDwords); add(mesh.provokingVertex); + add(mesh.outputPrimitiveSize); } add(state.stages.tessellation.has_value()); if (state.stages.tessellation) { @@ -701,7 +715,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, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation, context.shaderTerminateInvocation); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); @@ -832,6 +846,18 @@ void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten) { stats.indirectReadUs += readMs * 1000.0; } +std::array IndirectDrawCounts() { + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + return stats.indirect; +} + +std::array DrawPassTotals() { + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + return stats.passTotals; +} + void CountDrawSkip(DrawSkip kind, double us) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -925,11 +951,18 @@ void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCop recorder->KeepDrawSnapshot(address, copy.buffer->Bytes().size(), copy.generation, copy.registryGeneration, copy.buffer, use, derived); } -const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment) { +const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite) { auto* recorder = context.recorder; if (recorder == nullptr || bytes == 0) return nullptr; - GuestMemory::FlushGpuWrites(address, bytes); - if (recorder->PendingWriteOverlaps(address, bytes) || recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr; + bool behind = false; + if (behindGpuWrite == nullptr) { + GuestMemory::FlushGpuWrites(address, bytes); + if (recorder->PendingWriteOverlaps(address, bytes)) return nullptr; + } else { + StorageTexture::FlushPending(address, bytes, nullptr, "in-place vertex input"); + behind = recorder->PendingWriteOverlaps(address, bytes); + } + if (recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr; const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; if (context.copiedWriters != nullptr && std::any_of(context.copiedWriters->begin(), context.copiedWriters->end(), overlaps)) return nullptr; const auto drawWriters = DrawCopiedWriters(); @@ -937,6 +970,7 @@ const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address const auto* import = HostImportFor(context, address, bytes); if (import == nullptr || (address - import->base) % alignment != 0) return nullptr; recorder->NotePendingRead(address, bytes, Recorder::ReadKind::DrawInput); + if (behindGpuWrite != nullptr && behind) *behindGpuWrite = true; return import; } @@ -978,6 +1012,7 @@ struct DrawInputs { std::vector vertexHandles; std::vector vertexOffsets; std::vector> inPlaceReads; + bool vertexInputBehindGpuWrite = false; std::set fragmentOutputs; VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; @@ -1023,7 +1058,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.nothing = true; return inputs; } - } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); + } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation, "indirect draw on a non-vertex path must be resolved by the driver"); inputs.indexBytes = static_cast(draw.indexCount) * draw.indexSize; const auto indexBytes = inputs.indexBytes; APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast(indexBytes)); @@ -1031,6 +1066,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); + if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast(indexBytes), "an index buffer read"); APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; @@ -1180,7 +1216,8 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto [begin, end] = plan.copies[range]; const auto bytes = static_cast(end - begin); GuestMemory::CheckRange(reinterpret_cast(begin), bytes, 1); - if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range])) { + RequireNoMultisampleTarget(begin, bytes, "a vertex buffer read"); + if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) { rangeHandles.push_back(import->buffer); rangeOffsets.push_back(begin - import->base); inputs.inPlaceReads.emplace_back(begin, end); @@ -1537,6 +1574,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]); else residents[index]->MarkDirty(); } + for (const auto& resident : residents) { + if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue; + StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); + } } // What recordDraw records from: the recorder, the resources, the pipeline and framebuffer, the @@ -1730,9 +1771,15 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (!gpuIndirect) return false; return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end); }; - if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); + if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) { + recorder->FlushKeyStores(); + outcome.keyStoresFlushed = true; + } // A queued label store over such memory likewise (Recorder::RecordStore). - if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); + if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) { + recorder->FlushStores(); + outcome.storesFlushed = true; + } const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); const bool capture = CaptureInputsEnabled(); const bool meshIndirect = state.stages.mesh && args != nullptr; @@ -1749,7 +1796,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (target != nullptr) passAttachments.emplace_back(target->Image(), true); } const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); - const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownMemory}; + static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; + const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); + outcome.addressDraw = addressBased; + const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites(); + const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); const bool continued = start.continued; @@ -1793,6 +1844,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam countBarrier(1); } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); + if (inputs.vertexInputBehindGpuWrite) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); + countBarrier(1); + } if (meshIndirect) { meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); @@ -1989,7 +2044,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& timer.phase(PhaseSetup); // Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again. static const bool residentTargets = std::getenv("APS5_NO_RESIDENT_TARGETS") == nullptr; - if ((binding.gpuTiling || (color.tileMode != ColorTileMode::Linear && context.detiler != nullptr)) && residentTargets) { + static const bool residentLinear = std::getenv("APS5_NO_RESIDENT_LINEAR") == nullptr; + const bool linearResident = residentLinear && color.tileMode == ColorTileMode::Linear && ExistingHostImportFor(context, color.address, colorLayout.Bytes()) != nullptr; + if ((binding.gpuTiling || ((color.tileMode != ColorTileMode::Linear || linearResident) && context.detiler != nullptr)) && residentTargets) { // The lookup refreshes the image on every draw (StorageTexture::Refresh: FlushPending, // CollectWrites over the target's pages, the DCC key scan of TextureClearKeys, then // UnchangedSince). The page walk is skipped while the worker's collect epoch lasts @@ -2347,6 +2404,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& countBarrier(); } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); + if (inputs.vertexInputBehindGpuWrite) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); + countBarrier(); + } std::shared_ptr meshArguments; if (state.stages.mesh && args != nullptr) { meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 16604afe97..7dbac4e5ae 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -184,7 +184,6 @@ struct HostImports { bool unwatchImports = false; bool unwatchDmaBufImports = false; bool watchPrivateImports = false; -#ifdef _WIN32 VkDevice settleDevice = VK_NULL_HANDLE; VkBuffer settleTarget = VK_NULL_HANDLE; VkDeviceMemory settleMemory = VK_NULL_HANDLE; @@ -193,7 +192,6 @@ struct HostImports { PFN_vkDestroyFence destroyFence = nullptr; PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; PFN_vkFreeMemory settleFreeMemory = nullptr; -#endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; }; @@ -272,6 +270,28 @@ bool releaseParkedImports(HostImports& state, std::uint64_t begin, std::uint64_t return !released.empty(); } +#ifndef _WIN32 +struct ImportFamilies { + std::array indices{}; + std::uint32_t count = 0; +}; + +// A dma-buf import's buffer is used on the main queue, on the settle queue (settleImport) and, when +// sparse, on the sparse queue. +void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportFamilies& families, bool sparse) { + const auto add = [&](std::uint32_t family) { + if (std::find(families.indices.begin(), families.indices.begin() + families.count, family) == families.indices.begin() + families.count) families.indices[families.count++] = family; + }; + add(context.queueFamily); + if (context.settleQueue != VK_NULL_HANDLE) add(context.settleFamily); + if (sparse && context.sparseQueue != VK_NULL_HANDLE) add(context.sparseFamily); + const bool concurrent = families.count > 1; + info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = concurrent ? families.count : 0u; + info.pQueueFamilyIndices = concurrent ? families.indices.data() : nullptr; +} +#endif + const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); @@ -288,6 +308,10 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo // INDIRECT_BUFFER: DISPATCH_INDIRECT group counts are read in place (VulkanDevice::DispatchIndirect). info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +#ifndef _WIN32 + ImportFamilies families; + if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) shareDmaBufImport(context, info, families, false); +#endif if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements); @@ -466,7 +490,8 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; info.size = entry.bytes; info.usage = ChunkImportUsage; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + ImportFamilies families; + shareDmaBufImport(context, info, families, false); VkBuffer buffer = VK_NULL_HANDLE; if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) { failure = result; @@ -519,11 +544,8 @@ VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const Host info.flags = flags; info.size = size; info.usage = usage; - const std::uint32_t families[] = {context.queueFamily, context.sparseFamily}; - const bool concurrent = (flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0 && context.sparseQueue != VK_NULL_HANDLE; - info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; - info.queueFamilyIndexCount = concurrent ? 2u : 0u; - info.pQueueFamilyIndices = concurrent ? families : nullptr; + ImportFamilies families; + shareDmaBufImport(context, info, families, (flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0); VkBuffer buffer = VK_NULL_HANDLE; if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) throwSparseImport(entry, "vkCreateBuffer", result); VkMemoryRequirements requirements{}; @@ -650,9 +672,10 @@ ImportWatchRequest importWatchRequest() { } #endif -#ifdef _WIN32 constexpr std::uint64_t settleFloorBytes = 0x100000; +std::atomic settledImports{0}; + const char* settleImport(const Context& context, HostImports& state, const HostImport& entry, VkResult& result) { std::unique_lock queueLock(*context.settleQueueMutex); if (state.settleDevice != context.device) { @@ -703,6 +726,7 @@ const char* settleImport(const Context& context, HostImports& state, const HostI VkBuffer source = entry.buffer; VkBuffer alias = VK_NULL_HANDLE; const auto run = [&]() -> const char* { +#ifdef _WIN32 if (context.settleFamily != context.queueFamily) { VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; @@ -713,6 +737,7 @@ const char* settleImport(const Context& context, HostImports& state, const HostI if ((result = context.Function("vkBindBufferMemory")(context.device, alias, entry.memory, 0)) != VK_SUCCESS) return "vkBindBufferMemory (settle alias)"; source = alias; } +#endif if ((result = context.Function("vkResetFences")(context.device, 1, &state.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)"; if ((result = context.Function("vkResetCommandBuffer")(state.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)"; VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; @@ -731,9 +756,9 @@ const char* settleImport(const Context& context, HostImports& state, const HostI const char* step = run(); if (step != nullptr) context.Function("vkQueueWaitIdle")(context.settleQueue); if (alias != VK_NULL_HANDLE) context.Function("vkDestroyBuffer")(context.device, alias, nullptr); + if (step == nullptr) settledImports.fetch_add(1, std::memory_order_relaxed); return step; } -#endif void decideImportWatch(const Context& context, HostImports& state) { if (state.watchDevice == context.device) return; @@ -1000,6 +1025,7 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin return run; } +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); bool importsStale(const Context& context, const HostImports& state); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict); @@ -1032,8 +1058,8 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } 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; + if (const auto* low = peekImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; + if (const auto* high = peekImport(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; @@ -1055,15 +1081,17 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); } - // The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with - // what may be evicted) before any of them is retired. auto first = state.imports.lower_bound(base); if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; std::uint64_t replaced = 0; // A join may replace imports this build already looked up: it runs before any region confirms an import // (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the // hold (KeepThroughHold). Refusing it there would keep the run copied for good. - const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); }; + const auto held = [&](const HostImport& entry) { + if (joined || !pointerHeld(state, entry)) return false; + const bool expandedWindow = run.ranges.size() == 1 && entry.ranges.size() == 1 && entry.range == run.ranges.front() && base <= entry.base && base + bytes >= entry.base + entry.bytes && bytes > entry.bytes; + return !expandedWindow || Recorder::Active() == nullptr; + }; for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { if (held(it->second)) return nullptr; replaced += it->second.bytes; @@ -1131,6 +1159,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import state.unaliasedSpans.insert(base); return importAllocation(context, state, address, lease, end, evict); } + if (entry.alias == nullptr) return nullptr; } #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { @@ -1167,6 +1196,18 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return step == nullptr; }, watchedDmaBuf); } + if (step == nullptr && entry.dmaBuf && bytes >= settleFloorBytes && context.settleQueue != VK_NULL_HANDLE && context.settleQueueMutex != nullptr) { + static const bool traceSettle = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; + const auto start = std::chrono::steady_clock::now(); + if ((step = settleImport(context, state, entry, result)) != nullptr) { + destroyImport(context, entry); + entry.buffer = VK_NULL_HANDLE; + entry.memory = VK_NULL_HANDLE; + entry.chunk.reset(); + } else if (traceSettle) { + std::fprintf(stderr, "[gpu] settled the dma-buf import of 0x%llx+0x%llx on the settle queue in %.2f ms\n", static_cast(base), static_cast(bytes), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + } + } #endif entry.unwatched = step == nullptr && !(entry.dmaBuf ? watchedDmaBuf : watchedHostPointer); if (step != nullptr) { @@ -1269,14 +1310,18 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc 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) { +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) { auto found = state.imports.upper_bound(begin); if (found == state.imports.begin()) return nullptr; --found; const auto& entry = found->second; - if (begin < entry.base || end > entry.base + entry.bytes) return nullptr; - entry.lastUse = ++state.useCounter; - return &entry; + return begin < entry.base || end > entry.base + entry.bytes ? nullptr : &entry; +} + +const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { + const auto* entry = peekImport(state, begin, end); + if (entry != nullptr) entry->lastUse = ++state.useCounter; + return entry; } const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { @@ -2292,7 +2337,6 @@ void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); state.parked.erase(device); -#ifdef _WIN32 if (state.settleDevice == device) { if (state.settleFence != VK_NULL_HANDLE) state.destroyFence(device, state.settleFence, nullptr); if (state.settleTarget != VK_NULL_HANDLE) state.settleDestroyBuffer(device, state.settleTarget, nullptr); @@ -2303,7 +2347,6 @@ void ClearHostImports(VkDevice device) { state.settleCommands = VK_NULL_HANDLE; state.settleFence = VK_NULL_HANDLE; } -#endif if (state.device != device) return; for (const auto& [address, entry] : state.imports) { state.destroyBuffer(state.device, entry.buffer, nullptr); @@ -2555,6 +2598,10 @@ std::uint64_t HostImportLiveBytes() { return state.liveBytes; } +std::uint64_t HostImportsSettled() { + return settledImports.load(std::memory_order_relaxed); +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; ensurePinWaiter(); @@ -2572,6 +2619,14 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s return importAccess(context, state, address, address + bytes, lease); } +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { + if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) return nullptr; + return findImport(state, address, address + bytes); +} + bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (context.hostImportAlignment == 0) return true; ensurePinWaiter(); @@ -2631,7 +2686,7 @@ bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t std::lock_guard lock(state.mutex); // Imports of a replaced device are not this device's; a stale registry only means an import // may be dropped at the next reconcile, which the binding path handles. - return state.device == context.device && findImport(state, address, address + bytes) != nullptr; + return state.device == context.device && peekImport(state, address, address + bytes) != nullptr; } GuestBufferMemory::GuestBufferMemory(const Context& context) : context(context) {} @@ -2946,9 +3001,12 @@ void GuestBufferMemory::dissolveSpace(bool resolve) { std::vector merged; merged.reserve(space->base.size() + regions.size()); if (prepared) { - // Both sorted and disjoint (UploadPrepare merged this build's regions; the space's are - // registered ranges), so the merge keeps the order Descriptor searches. std::merge(space->base.begin(), space->base.end(), std::make_move_iterator(regions.begin()), std::make_move_iterator(regions.end()), std::back_inserter(merged), [](const Region& left, const Region& right) { return left.begin < right.begin; }); + std::uint64_t reached = 0; + for (const auto& region : merged) { + regionsOverlap = regionsOverlap || region.begin < reached; + reached = std::max(reached, region.end); + } } else { merged.assign(space->base.begin(), space->base.end()); merged.insert(merged.end(), std::make_move_iterator(regions.begin()), std::make_move_iterator(regions.end())); @@ -2965,15 +3023,22 @@ void GuestBufferMemory::dissolveSpace(bool resolve) { space.reset(); } -const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address) const { - // The region with the greatest begin at or below the address, as the one merged list gave; - // the caller checks the end, as before. - const auto candidate = [address](const std::vector& list) -> const Region* { +const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address, std::uint64_t end) const { + const auto candidate = [address, end](const std::vector& list, bool overlapping) -> const Region* { const auto found = std::upper_bound(list.begin(), list.end(), address, [](std::uint64_t value, const Region& region) { return value < region.begin; }); - return found == list.begin() ? nullptr : &*std::prev(found); + if (found == list.begin()) return nullptr; + if (overlapping) { + for (auto it = found; it != list.begin();) { + --it; + if (end <= it->end) return &*it; + } + } + return &*std::prev(found); }; - const auto* own = candidate(regions); - const auto* shared = space != nullptr ? candidate(space->base) : nullptr; + const auto* own = candidate(regions, regionsOverlap); + const auto* shared = space != nullptr ? candidate(space->base, false) : nullptr; + const auto holds = [end](const Region* region) { return region != nullptr && end <= region->end; }; + if (holds(own) != holds(shared)) return holds(own) ? own : shared; if (own == nullptr || (shared != nullptr && shared->begin > own->begin)) return shared; return own; } @@ -3684,7 +3749,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { const auto& lease = importRanges(); const auto index = leasedIndexContaining(lease, region.begin); if (entry == nullptr && index != lease.size() && lease[index]->address == region.begin) { - const auto* low = findImport(state, region.begin, region.begin + 1); + const auto* low = peekImport(state, region.begin, region.begin + 1); const auto joinBase = low != nullptr && low->joined ? low->base : region.begin; const auto joins = state.sparseRunJoins.find(joinBase); const auto made = joins == state.sparseRunJoins.end() ? 0u : joins->second; @@ -3792,7 +3857,7 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { if (profile && context.hostImportAlignment != 0) { auto& state = Imports(); std::lock_guard lock(state.mutex); - inImport = findImport(state, region.begin, region.end) != nullptr; + inImport = peekImport(state, region.begin, region.end) != nullptr; } const auto allocateStart = std::chrono::steady_clock::now(); if (region.sparse && region.buffer == nullptr && (region.hostBacked || region.writable)) { @@ -4036,7 +4101,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const { Require(uploaded && !committed, "guest GPU memory is not available"); Require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest buffer view"); - const auto* found = owner(address); + const auto* found = owner(address, address + bytes); Require(found != nullptr, "guest buffer has no GPU owner"); const auto& region = *found; const auto describe = [&](const char* what) { @@ -4064,7 +4129,10 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: } std::uint64_t GuestBufferMemory::ViewBytes(std::uint64_t bytes, std::uint32_t adjustment) { - return adjustment % 4 == 0 ? bytes + adjustment : 4 * (adjustment / 4 + bytes / 4 + 1); + if (adjustment % 4 == 0) return bytes + adjustment; + const auto dwords = adjustment / 4 + bytes / 4 + (adjustment % 4 + bytes % 4 + 3) / 4; + Require(dwords <= std::numeric_limits::max() / 4, "guest buffer view range overflow"); + return 4 * dwords; } ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) { @@ -4080,16 +4148,73 @@ std::uint64_t GuestBufferMemory::servedEnd(const Region& region) { return region.bufferEnd != 0 ? region.bufferEnd : region.end; } +namespace { + +using AddressRange = ShaderRecompiler::BdaAbi::Range; + +void AppendFrom(const AddressRange& range, std::uint64_t begin, std::uint64_t end, std::vector& result) { + if (begin >= end) return; + auto part = range; + part.begin = begin; + part.end = end; + part.deviceAddress += begin - range.begin; + result.push_back(part); +} + +void AppendDisjoint(const std::vector& ranges, std::vector& result) { + std::uint64_t reached = 0; + for (const auto& range : ranges) { + AppendFrom(range, std::max(range.begin, reached), range.end, result); + reached = std::max(reached, range.end); + } +} + +void AppendUncovered(const std::vector& ranges, const std::vector& covering, std::vector& result) { + for (const auto& range : ranges) { + auto cursor = range.begin; + auto cover = std::partition_point(covering.begin(), covering.end(), [&](const AddressRange& candidate) { return candidate.end <= range.begin; }); + for (; cover != covering.end() && cover->begin < range.end && cursor < range.end; ++cover) { + AppendFrom(range, cursor, std::min(cover->begin, range.end), result); + cursor = std::max(cursor, cover->end); + } + AppendFrom(range, cursor, range.end, result); + } +} + +std::vector MergeByBegin(const std::vector& left, const std::vector& right) { + std::vector merged; + merged.reserve(left.size() + right.size()); + std::merge(left.begin(), left.end(), right.begin(), right.end(), std::back_inserter(merged), [](const AddressRange& first, const AddressRange& second) { return first.begin < second.begin; }); + return merged; +} + +} + std::vector GuestBufferMemory::AddressRanges() const { Require(uploaded && !committed, "guest GPU address ranges are not available"); std::vector result; - result.reserve(regions.size() + (space != nullptr ? space->ranges.size() : 0)); - for (const auto& region : regions) result.push_back(addressRange(region)); if (space != nullptr) { - std::vector combined; - combined.reserve(result.size() + space->ranges.size()); - std::merge(space->ranges.begin(), space->ranges.end(), result.begin(), result.end(), std::back_inserter(combined), [](const auto& left, const auto& right) { return left.begin < right.begin; }); - result = std::move(combined); + std::vector own; + own.reserve(regions.size()); + for (const auto& region : regions) own.push_back(addressRange(region)); + std::vector uncovered; + uncovered.reserve(own.size()); + AppendUncovered(own, space->ranges, uncovered); + result = MergeByBegin(space->ranges, uncovered); + } else if (regionsOverlap) { + std::vector live; + std::vector copies; + for (const auto& region : regions) (region.direct != nullptr || region.mirror != nullptr ? live : copies).push_back(addressRange(region)); + std::vector liveParts; + AppendDisjoint(live, liveParts); + std::vector copyParts; + AppendDisjoint(copies, copyParts); + std::vector uncovered; + AppendUncovered(copyParts, liveParts, uncovered); + result = MergeByBegin(liveParts, uncovered); + } else { + result.reserve(regions.size()); + for (const auto& region : regions) result.push_back(addressRange(region)); } if (writes.empty()) return result; auto sortedWrites = writes; @@ -4116,7 +4241,7 @@ std::vector GuestBufferMemory::AddressRanges() part.end = end; part.deviceAddress += cursor - range.begin; if (written) { - const auto* region = owner(cursor); + const auto* region = owner(cursor, end); Require(region != nullptr && region->writable && (region->mirror == nullptr || region->mirror->writable), "runtime buffer write has no writable GPU owner"); part.permissions |= ShaderRecompiler::BdaAbi::Write; } @@ -4176,7 +4301,7 @@ std::optional GuestBufferMemory::CachedAddressTa bool GuestBufferMemory::HasCopiedWrites() const { for (const auto& [begin, end] : writes) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; if (region.direct != nullptr) continue; @@ -4196,7 +4321,7 @@ bool GuestBufferMemory::CopiedWritesOverlap(std::uint64_t address, std::size_t b if (bytes == 0) return false; for (const auto& [begin, end] : writes) { if (!(address < end && begin < address + bytes)) continue; - const auto* found = owner(begin); + const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; if (region.direct != nullptr) continue; @@ -4209,7 +4334,7 @@ bool GuestBufferMemory::CopiedWritesOverlap(std::uint64_t address, std::size_t b void GuestBufferMemory::MarkDirectWrites() const { for (const auto& [begin, end] : writes) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast(end - begin)); @@ -4276,7 +4401,7 @@ std::vector> mergeWrites(std::vectormirror == nullptr || !found->mirror->heap || !found->mirror->writable) continue; Require(end <= found->end, "written range exceeds its heap mirror"); const auto& mirror = *found->mirror; @@ -4293,7 +4418,7 @@ void GuestBufferMemory::WriteBack() { // The CPU keeps running while the GPU works, so storing whole ranges would roll back its writes to // bytes the shader never touched. Only runs that differ from the uploaded copy are stored. for (const auto& [begin, end] : merged) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); Require(found != nullptr, "write-back range has no GPU owner"); const auto& region = *found; // Every branch below but the direct, copied-back and staged ones stores from the CPU. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index ac536cc811..413eeced6a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -116,7 +116,6 @@ GuestSamplerResource DecodeSamplerResource(std::span words, Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented"); const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter); - Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented"); auto anisoRatio = 1.0f; if (aniso) { if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 13093680d6..9b92e9b857 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -55,7 +55,7 @@ bool IsNullTextureResource(std::span words) { Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; const auto type = (words[3] >> 28u) & 0xfu; - return base40 == 0 || type == 0; + return base40 == 0 || type <= 1; } GuestTextureResource DecodeTextureResource(std::span words) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 15da911cfc..b85b734633 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -53,55 +53,35 @@ class MultisampleTarget { public: MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { this->context.bufferPool.reset(); + if (target.dccAddress != 0) metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(target); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); VkFormatProperties properties{}; context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); - VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VkImageFormatProperties supported{}; sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); + storageFormat = HostStorageFormat(context, target.format); + storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + const auto storageSupports = [&](VkFormat format, VkImageUsageFlags formatUsage, VkImageCreateFlags flags) { + VkImageFormatProperties properties{}; + return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0; + }; + if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED; try { - VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - info.imageType = VK_IMAGE_TYPE_2D; - info.format = target.format; - info.extent = {target.extent.width, target.extent.height, 1}; - info.mipLevels = 1; - info.arrayLayers = 1; - info.samples = samples; - info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = usage; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target"); - 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 multisampled target"); - Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target"); - VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - viewInfo.image = image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = target.format; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target"); + const auto created = allocate(usage, 0u); + image = created.image; + memory = created.memory; + view = created.view; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { - VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; - toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toGeneral.image = image; - toGeneral.subresourceRange = viewInfo.subresourceRange; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); - const VkClearColorValue zero{}; - context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange); + recordToGeneral(commands, image); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); }); @@ -114,7 +94,90 @@ public: MultisampleTarget(const MultisampleTarget&) = delete; MultisampleTarget& operator=(const MultisampleTarget&) = delete; + struct Allocation { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + }; + + Allocation allocate(VkImageUsageFlags imageUsage, VkImageCreateFlags flags) { + Allocation created; + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + 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 = imageUsage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &created.image), "vkCreateImage multisampled target"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, created.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, &created.memory), "vkAllocateMemory multisampled target"); + Check(context.Function("vkBindImageMemory")(context.device, created.image, created.memory, 0), "vkBindImageMemory multisampled target"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = created.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target"); + } catch (...) { + destroy(created); + throw; + } + return created; + } + + void destroy(const Allocation& allocation) noexcept { + if (allocation.view) context.Function("vkDestroyImageView")(context.device, allocation.view, nullptr); + if (allocation.image) context.Function("vkDestroyImage")(context.device, allocation.image, nullptr); + if (allocation.memory) context.Function("vkFreeMemory")(context.device, allocation.memory, nullptr); + } + + void recordToGeneral(VkCommandBuffer commands, VkImage destination) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = destination; + toGeneral.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + } + + void makeStorable(Recorder& recorder) { + if ((usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) return; + const auto storable = allocate(usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags); + const auto source = image; + recordToGeneral(recorder.Commands(), storable.image); + RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(recorder.Commands(), source, VK_IMAGE_LAYOUT_GENERAL, storable.image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + for (const auto& [key, texture] : textures) recorder.Keep(texture); + textures.clear(); + retired.push_back({image, memory, view}); + image = storable.image; + memory = storable.memory; + view = storable.view; + usage |= VK_IMAGE_USAGE_STORAGE_BIT; + } + void ApplyFastClear(const ColorTarget& use) { + checkDccCurrent(use); VkClearColorValue clear{}; if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { @@ -126,11 +189,50 @@ public: }); } + void SettleStorageWrites() { + if (!storageWritten) return; + storageWritten = false; + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + } + + VkImageView Storage(VkFormat format) { + Require(storageFormat != VK_FORMAT_UNDEFINED, "the device cannot write the multisampled color target's format as a storage image at its sample count"); + Require(format == storageFormat, "a storage image access to a multisampled color target in another format than the target's storage form is not implemented"); + Require(cmask.Refusal().empty(), cmask.Refusal()); + Require(cmask.Keys() != DccKeys::Clear0000, "a storage image access to a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); + auto* recording = Recorder::Active(); + Require(recording != nullptr, "a multisampled color target is only written by recorded dispatches"); + makeStorable(*recording); + ApplyFastClear(target); + if (storageView == VK_NULL_HANDLE) { + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = storageFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage"); + } + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + storageWritten = true; + return storageView; + } + + bool Storable() const { + return (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0; + } + std::shared_ptr Sampled(VkComponentMapping components) { Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); Require(cmask.Refusal().empty(), cmask.Refusal()); Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches"); + SettleStorageWrites(); ApplyFastClear(target); recordCommands(context, [&](VkCommandBuffer commands, Recorder*) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); @@ -178,6 +280,7 @@ public: Context context; std::shared_ptr> active = std::make_shared>(true); + std::shared_ptr> metadataValid; ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; @@ -185,17 +288,73 @@ public: VkRenderPass resolvePass = VK_NULL_HANDLE; MultisampledCmask cmask; MultisampledColorFill colorFill; + bool fromTexture = false; private: bool sampleable = false; + VkImageUsageFlags usage = 0; + VkFormat storageFormat = VK_FORMAT_UNDEFINED; + VkImageCreateFlags storageFlags = 0; + std::vector retired; + VkImageView storageView = VK_NULL_HANDLE; + bool storageWritten = false; std::map, std::shared_ptr> textures; + std::vector dccRanges; + + VkClearColorValue initialDccClear(const ColorTarget& use) { + if (use.dccAddress == 0) return {}; + const auto dccBytes = MultisampledDccBytes(use); + Require((use.dccAddress & 4095u) == 0, "multisampled DCC metadata base alignment is not implemented"); + Require(use.cmaskAddress == 0 ? use.cmaskBytes == 0 : use.cmaskBytes == CmaskBytes(use.extent.width, use.extent.height), "multisampled DCC CMASK footprint is invalid"); + const auto colorBytes = MultisampledColorBytes(use); + Require(colorBytes != 0, "multisampled DCC color footprint is not implemented"); + dccRanges.push_back({use.address, static_cast(colorBytes), 0}); + dccRanges.push_back({use.dccAddress, dccBytes, 0}); + if (use.cmaskAddress != 0) dccRanges.push_back({use.cmaskAddress, use.cmaskBytes, 0}); + for (auto& range : dccRanges) { + GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); + Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); + } + for (std::size_t first = 0; first < dccRanges.size(); ++first) { + for (std::size_t second = first + 1; second < dccRanges.size(); ++second) { + const auto& a = dccRanges[first]; + const auto& b = dccRanges[second]; + Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented"); + } + } + std::vector dcc(dccRanges[1].bytes); + GuestMemory::Read(use.dccAddress, dcc); + std::vector cmask(use.cmaskBytes); + if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); + const auto clear = MultisampledDccClear(use, dcc, cmask); + Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import"); + return clear; + } + + void checkDccCurrent(const ColorTarget& use) const { + if (dccRanges.empty()) { + Require(use.dccAddress == 0, "adding DCC to a resident multisampled color target is not implemented"); + return; + } + Require(metadataValid->load(std::memory_order_acquire), "multisampled DCC metadata was released while its color target remains resident"); + static_cast(MultisampledDccBytes(use)); + Require(use.address == target.address && use.dccAddress == target.dccAddress && use.dccControl == target.dccControl && use.dccAlphaOnMsb == target.dccAlphaOnMsb && use.dccPipeAligned == target.dccPipeAligned && use.cmaskAddress == target.cmaskAddress && use.cmaskBytes == target.cmaskBytes && use.tileMode == target.tileMode && use.elementBytes == target.elementBytes && use.pipeBankXor == target.pipeBankXor, "multisampled DCC target layout or metadata changed"); + for (const auto& range : dccRanges) { + Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented"); + } + } void release() noexcept { textures.clear(); if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); - if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); + destroy({image, memory, view}); + for (const auto& allocation : retired) destroy(allocation); } }; @@ -205,6 +364,7 @@ struct GuardedRange { std::uint64_t bytes; std::uint32_t samples; std::shared_ptr> active; + std::shared_ptr> metadataValid; }; class PrimedSharedMutex : public std::shared_mutex { @@ -238,14 +398,16 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { + if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; - range.active->store(false, std::memory_order_release); + if (!range.metadataValid) range.active->store(false, std::memory_order_release); return true; }); + std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); anyGuarded().store(!guarded().empty(), std::memory_order_release); } -void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active) { +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid) { 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); @@ -258,6 +420,10 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ } std::unique_lock lock(guardedMutex()); guarded().push_back({context.device, target.address, bytes, target.samples, active}); + if (target.dccAddress != 0) { + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); + } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); } @@ -276,22 +442,27 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (target->fromTexture && use.cmaskAddress != 0) { + target->fromTexture = false; + target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + } target->cmask.CheckAddress(use.cmaskAddress); + target->SettleStorageWrites(); target->ApplyFastClear(use); return *target; } targets().push_back(std::make_unique(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)); + guard(context, use, created.active, created.metadataValid); + if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; } -MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format) { +MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access) { const auto refuse = [&](const std::string& reason) { - char text[160]; - std::snprintf(text, sizeof(text), "AGC graphics: sampling the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); + char text[192]; + std::snprintf(text, sizeof(text), "AGC graphics: %s the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", access, resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); throw std::runtime_error(text + reason + ", is not implemented"); }; ColorTarget use{}; @@ -332,8 +503,9 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); - guard(context, use, created.active); + guard(context, use, created.active, created.metadataValid); created.colorFill.NoteFill(use.address, static_cast(bytes), value); + created.fromTexture = true; return created; } @@ -406,6 +578,45 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target) { return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2; } +std::size_t MultisampledDccBytes(const ColorTarget& target) { + Require(target.samples == 2 || target.samples == 4 || target.samples == 8, "unsupported multisampled DCC fragment count"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.dccPipeAligned, "multisampled DCC requires pipe-aligned SW_64KB_R_X"); + Require(std::has_single_bit(target.elementBytes) && target.elementBytes <= 16, "unsupported multisampled DCC element size"); + Require(target.extent.width != 0 && target.extent.width <= 16384 && target.extent.height != 0 && target.extent.height <= 16384, "invalid multisampled DCC extent"); + Require(target.mipCount == 1 && target.mip == 0 && !target.mipTail && target.depth == 1 && target.depthSlice == 0 && target.arrayLastSlice == 0 && target.arraySlice == 0 && target.layers == 1, "multisampled DCC array, volume or mip contents are not implemented"); + const auto bits = 19u - static_cast(std::countr_zero(target.elementBytes)); + const std::uint64_t width = std::uint64_t{1} << ((bits + 1u) / 2u); + const std::uint64_t height = std::uint64_t{1} << (bits / 2u); + return static_cast(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u); +} + +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask) { + Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented"); + Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch"); + Require(std::ranges::all_of(dcc, [&](std::byte key) { return key == dcc.front(); }), "mixed multisampled DCC metadata is not implemented"); + Require(target.cmaskAddress == 0 ? cmask.empty() && target.cmaskBytes == 0 : cmask.size() == target.cmaskBytes && target.cmaskBytes == CmaskBytes(target.extent.width, target.extent.height), "multisampled DCC CMASK snapshot size mismatch"); + if (!cmask.empty()) { + const auto code = std::to_integer(cmask.front()); + const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu; + Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented"); + } + DccKeys keys; + switch (std::to_integer(dcc.front())) { + case 0x00u: keys = DccKeys::Clear0000; break; + case 0x40u: keys = DccKeys::Clear0001; break; + case 0x80u: keys = DccKeys::Clear1110; break; + case 0xc0u: keys = DccKeys::Clear1111; break; + default: throw std::runtime_error("initial multisampled DCC contents are not a supported uniform fast clear"); + } + std::array texels{}; + Require(FillDccClear(target.format, keys, target.dccAlphaOnMsb, texels), "multisampled DCC clear format is not implemented"); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value"); + return clear; +} + bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) { const std::array pattern{fill, fill, fill, fill}; VkFormat unorm = VK_FORMAT_UNDEFINED; @@ -478,7 +689,7 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); - return createFilled(context, resource, format).Sampled(components); + return createFilled(context, resource, format, "sampling").Sampled(components); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -490,6 +701,33 @@ void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget acquire(context, target); } +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) { + std::lock_guard lock(targetsMutex()); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format)); + other = target.get(); + } + if (other == nullptr) { + Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches"); + return createFilled(context, resource, format, "writing").Storage(StorageFormatForGuest(context, resource.format)); + } + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); + throw std::runtime_error(text); +} + +bool MultisampleTargetStorable(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + return std::any_of(targets().begin(), targets().end(), [&](const auto& candidate) { + return candidate->active->load(std::memory_order_acquire) && candidate->context.device == context.device && sameTarget(candidate->target, target) && candidate->Storable(); + }); +} + void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp index b4fce71845..7f83917b88 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp @@ -5,14 +5,21 @@ #include #include #include +#include namespace AgcDriver::Graphics { namespace { +std::atomic runningHelpers{0}; + class ComparePool { public: explicit ComparePool(unsigned count) { - for (unsigned i = 0; i < count; ++i) std::thread([this] { work(); }).detach(); + stopping = count == 0; + for (unsigned i = 0; i < count; ++i) { + workers.emplace_back([this] { work(); }); + runningHelpers.fetch_add(1, std::memory_order_acq_rel); + } } bool Run(std::span spans, std::span equal) { @@ -20,6 +27,7 @@ public: if (!caller.owns_lock()) return false; { std::unique_lock lock(mutex); + if (stopping) return false; finished.wait(lock, [&] { return working == 0; }); jobSpans = spans; jobEqual = equal; @@ -36,6 +44,21 @@ public: return true; } + void Stop() { + std::vector joined; + { + std::lock_guard caller(callers); + std::lock_guard lock(mutex); + stopping = true; + joined.swap(workers); + } + wake.notify_all(); + for (auto& worker : joined) { + if (worker.get_id() == std::this_thread::get_id()) worker.detach(); + else worker.join(); + } + } + private: void drain(std::span spans, std::span equal) { std::size_t compared = 0; @@ -51,7 +74,8 @@ private: std::uint64_t seen = 0; std::unique_lock lock(mutex); for (;;) { - wake.wait(lock, [&] { return generation != seen; }); + wake.wait(lock, [&] { return generation != seen || stopping; }); + if (stopping) break; seen = generation; const auto spans = jobSpans; const auto equal = jobEqual; @@ -62,6 +86,7 @@ private: --working; if (working == 0) finished.notify_all(); } + runningHelpers.fetch_sub(1, std::memory_order_acq_rel); } std::mutex callers; @@ -74,10 +99,32 @@ private: std::span jobEqual; std::atomic next{0}; std::atomic done{0}; + std::vector workers; + bool stopping = false; }; +std::mutex& poolLifecycle() { + static std::mutex lifecycle; + return lifecycle; +} + +bool& poolStopped() { + static bool stopped = false; + return stopped; +} + +std::atomic& createdPool() { + static std::atomic created{nullptr}; + return created; +} + ComparePool* sharedPool() { - static ComparePool* const pool = new ComparePool(CompareHelpers()); + static ComparePool* const pool = [] { + std::lock_guard lifecycle(poolLifecycle()); + auto* created = new ComparePool(poolStopped() ? 0u : CompareHelpers()); + createdPool().store(created, std::memory_order_release); + return created; + }(); return pool; } @@ -97,6 +144,20 @@ void CompareSpansFrom(std::span spans, std::spanStop(); +} + +unsigned CompareHelpersRunning() { + return runningHelpers.load(std::memory_order_acquire); +} + void CompareSpans(std::span spans, std::span equal) { CompareSpansFrom(spans, equal, std::size_t{1} << 20u); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 9fd5790a24..fce52e220e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,9 +2,11 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -104,8 +106,8 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp return; } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); - Require(!state.rectList && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); - Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); + Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); + Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; Require(shader.program != nullptr, "missing compiled fragment shader"); @@ -118,7 +120,7 @@ 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"); Require(!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0, "sample locations varying between pixels require a 2x2 sample location grid for the draw's sample count"); - const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, HostDepthFormat(context, state.depth->format), state.depth->samples)); + const bool enabled = extensionSupportsSamples && (state.samples > 1 || state.customSampleLocations) && (!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; } @@ -193,10 +195,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; keys = {}; for (const auto& shader : shaders) { - if (shader.stage == Stage::Geometry || shader.program->PipelineVariantId() == 0) return false; + const auto identity = shader.program->Identity(); + if (shader.stage == Stage::Geometry || identity.Empty()) return false; auto& key = shader.stage == Stage::Fragment ? keys.fragmentShader : keys.preRasterization; appendKey(key, shader.stage); - appendKey(key, shader.program->PipelineVariantId()); + appendKey(key, identity.low); + appendKey(key, identity.high); appendKey(key, shader.pushConstantOffset); if (resources.split) { auto& sets = shader.stage == Stage::Fragment ? keys.fragmentSets : keys.preRasterizationSets; @@ -711,6 +715,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V } void Pipeline::Continue(VkCommandBuffer commands, const State& state) const { + if (optimized != nullptr) optimized->Schedule(); const auto best = optimized != nullptr ? optimized->handle.load() : VK_NULL_HANDLE; context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, best != VK_NULL_HANDLE ? best : pipeline); context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &state.viewport); @@ -779,25 +784,27 @@ void append(std::vector& key, const TValue& value) { key.insert(key.end(), bytes.begin(), bytes.end()); } -// Everything the Pipeline objects are built from, or empty when a stage's result has no variant id -// (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list -// control and evaluation stages are generated from the vertex and fragment results, which the key -// already names, so they carry no id of their own. +// Everything the Pipeline objects are built from, or empty when a stage's result has no module +// (nothing else may share its pipeline). The stage identities and the state fields are the same on +// every run (CanonicalPipelineState keeps exactly the fields read here), which lets the journal's +// replay insert an entry a later draw finds; a generated stage (rect-list pair, geometry) is keyed +// by its identity like the others, so a replayed pipeline built from another device's generated +// module is missed rather than used. void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { - using Stage = ShaderRecompiler::ShaderStage; key.clear(); append(key, context.device); append(key, attachmentLayout); append(key, shaders.size()); for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - const bool generated = (state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation)) || shader.stage == Stage::Geometry; - if (!generated && shader.program->PipelineVariantId() == 0) { + const auto identity = shader.program->Identity(); + if (identity.Empty()) { key.clear(); return; } append(key, shader.stage); - append(key, generated ? std::uint64_t{0} : shader.program->PipelineVariantId()); + append(key, identity.low); + append(key, identity.high); // Where the stage's push constants sit in the block (AssemblePushConstants). append(key, shader.pushConstantOffset); } @@ -840,18 +847,23 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.sampleLocations); } append(key, state.colors.size()); - for (const auto& color : state.colors) append(key, color.format); + for (const auto& color : state.colors) { + append(key, color.format); + append(key, color.componentMapping); + } if (state.blends.size() != state.colors.size()) { for (const auto& color : state.colors) append(key, color.exportIndex); } append(key, state.depth.has_value()); if (state.depth) { append(key, state.depth->format); + append(key, state.depth->samples); append(key, state.depthTest); append(key, state.depthWrite); append(key, state.depthCompare); append(key, state.depthBoundsTest); append(key, state.depthBias); + append(key, state.depthBias && state.depthBiasClamp != 0.0f); append(key, state.depthBiasPerFace); append(key, state.stencilTest); auto front = state.stencilFront; @@ -872,6 +884,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, mesh.threadsPerGroup); append(key, mesh.ldsSizeDwords); append(key, mesh.provokingVertex); + append(key, mesh.outputPrimitiveSize); } append(key, state.stages.tessellation.has_value()); if (state.stages.tessellation) { @@ -898,12 +911,12 @@ struct PipelineStore { // The device's buffer pool at insertion: it is made and reset with the device, so it tells // the device instance apart from a later one the loader gave the same handle value. std::weak_ptr pool; + bool tracksPool; std::uint64_t hash; std::vector key; std::shared_ptr pipeline; }; std::mutex mutex; - std::vector scratchKey; // Least recently used first. std::list entries; std::unordered_map::iterator> index; @@ -911,6 +924,7 @@ struct PipelineStore { std::uint64_t misses = 0; std::uint64_t uncached = 0; std::uint64_t evicted = 0; + PipelineStoreCounters totals; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; @@ -923,8 +937,12 @@ PipelineStore& Pipelines() { // Whether the entry's objects belong to the device the context names. A context without a buffer // pool (tests) is identified by the handle alone. -bool alive(const PipelineStore::Entry& entry, const Context& context) { +bool alive(PipelineStore::Entry& entry, const Context& context) { if (entry.device != context.device) return false; + if (!entry.tracksPool && context.bufferPool != nullptr) { + entry.pool = context.bufferPool; + entry.tracksPool = true; + } return context.bufferPool == nullptr || entry.pool.lock() == context.bufferPool; } @@ -936,6 +954,11 @@ std::list::iterator abandon(PipelineStore& store, std::lis return store.entries.erase(it); } +std::size_t storeBound() { + static const auto bound = static_cast(std::max(1u, PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u))); + return bound; +} + void reportPipelines(PipelineStore& store) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -953,57 +976,117 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout); } -std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout, bool* created) { static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr; + if (created != nullptr) *created = false; if (disabled) return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); - std::lock_guard lock(store.mutex); - reportPipelines(store); - auto& key = store.scratchKey; + struct PipelineKeyStorage {}; + auto& key = HostThreadLocal, PipelineKeyStorage>(); pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout); if (key.empty()) { + std::lock_guard lock(store.mutex); ++store.uncached; + ++store.totals.uncached; return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); } const auto hash = hashKey(key); - if (const auto found = store.index.find(hash); found != store.index.end()) { + // The store's entry of the key, or nullptr with `found` telling a miss (the key is new) from a + // collision (another configuration owns the hash, so this one stays private). + const auto lookup = [&](bool& collision) -> std::shared_ptr { + collision = false; + const auto found = store.index.find(hash); + if (found == store.index.end()) return nullptr; const auto it = found->second; - if (it->key == key) { - // The key names the device handle, which the loader may reuse for a device created after - // this one was destroyed without ClearCachedPipelines: such an entry is a miss. - if (alive(*it, context)) { - ++store.hits; - store.entries.splice(store.entries.end(), store.entries, it); - return it->pipeline; - } - abandon(store, it); - } else { + if (it->key != key) { + collision = true; + return nullptr; + } + // The key names the device handle, which the loader may reuse for a device created after + // this one was destroyed without ClearCachedPipelines: such an entry is a miss. + if (alive(*it, context)) { + store.entries.splice(store.entries.end(), store.entries, it); + return it->pipeline; + } + abandon(store, it); + return nullptr; + }; + { + std::lock_guard lock(store.mutex); + reportPipelines(store); + bool collision = false; + if (auto pipeline = lookup(collision)) { + ++store.hits; + ++store.totals.hits; + return pipeline; + } + if (collision) { // A different configuration with the same hash keeps the resident entry; this one stays private. ++store.uncached; + ++store.totals.uncached; return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); } + ++store.misses; + ++store.totals.misses; } - ++store.misses; - // Entries of another device belong to one the driver replaced (it does so under the GpuMutex - // before any draw reaches the new device), whose objects went with it: forget them. - for (auto it = store.entries.begin(); it != store.entries.end();) { - it = alive(*it, context) ? std::next(it) : abandon(store, it); - } + // Built outside the mutex: a draw that finds its pipeline resident is not held behind another + // thread's build (the journal replay builds here too), and a device's driver compiles in + // parallel when several threads build at once. auto pipeline = std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); - store.entries.push_back({context.device, context.bufferPool, hash, key, pipeline}); - store.index[hash] = std::prev(store.entries.end()); - constexpr std::size_t bound = 1024; - while (store.entries.size() > bound) { - // Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence). - const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; }); - if (victim == store.entries.end()) break; - store.index.erase(victim->hash); - store.entries.erase(victim); - ++store.evicted; + const auto bound = storeBound(); + const bool replaying = PipelineJournalReplaying(); + { + std::lock_guard lock(store.mutex); + bool collision = false; + if (auto raced = lookup(collision)) return raced; + if (collision) { + ++store.uncached; + ++store.totals.uncached; + return pipeline; + } + // Entries of another device belong to one the driver replaced (it does so under the GpuMutex + // before any draw reaches the new device), whose objects went with it: forget them. + for (auto it = store.entries.begin(); it != store.entries.end();) { + it = alive(*it, context) ? std::next(it) : abandon(store, it); + } + // A replayed pipeline enters at the least recently used end and never displaces a draw's: + // the title's own pipelines evict it first, and a full store takes none. + if (replaying && store.entries.size() >= bound) return pipeline; + if (replaying) { + store.entries.push_front({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); + store.index[hash] = store.entries.begin(); + } else { + store.entries.push_back({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); + store.index[hash] = std::prev(store.entries.end()); + } + while (store.entries.size() > bound) { + // Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence). + const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; }); + if (victim == store.entries.end()) break; + store.index.erase(victim->hash); + store.entries.erase(victim); + ++store.evicted; + ++store.totals.evicted; + } } + if (created != nullptr) *created = true; + if (PipelineJournalEnabled() && !PipelineJournalReplaying()) RecordGraphicsPipeline(MakeGraphicsPipelineRecipe(state, vertexInput, resources, shaders, attachmentLayout), shaders); return pipeline; } +void BuildPipelineStoreKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { + pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout); +} + +PipelineStoreCounters PipelineStoreTotals() { + auto& store = Pipelines(); + std::lock_guard lock(store.mutex); + auto totals = store.totals; + totals.cached = store.entries.size(); + totals.bound = storeBound(); + return totals; +} + void ClearCachedPipelines(VkDevice device) { auto& store = Pipelines(); std::lock_guard lock(store.mutex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index 8b5d426e59..a3cf852d6a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -35,7 +35,7 @@ std::mutex& pathMutex(const std::filesystem::path& path) { } -PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties) : context(context), properties(properties) { +PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile) : context(context), properties(properties) { static_assert(sizeof(FileHeader) == 24); const auto directory = ShaderRecompiler::ShaderCacheDirectory(); if (!directory.empty()) { @@ -52,7 +52,7 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::vector initialData; if (!path.empty()) { std::lock_guard pathLock(pathMutex(path)); - load(initialData); + if (loadFromFile) load(initialData); } VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; info.initialDataSize = initialData.size(); @@ -145,7 +145,7 @@ std::optional PipelineCache::readHeader() const { } void PipelineCache::load(std::vector& initialData) { - const auto file = readFile(); + auto file = readFile(); if (file.state == FileState::Missing) return; if (file.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; starting empty\n", path.string().c_str()); @@ -155,11 +155,12 @@ void PipelineCache::load(std::vector& initialData) { std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), file.invalidReason); return; } - const auto data = file.Payload(); - initialData.assign(data.begin(), data.end()); + initialData = std::move(file.contents); + initialData.erase(initialData.begin(), initialData.begin() + sizeof(FileHeader)); loadedBytes = initialData.size(); settledBytes = initialData.size(); settledFile = file.header; + holdsFile = true; std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast(initialData.size()) / 1024.0, path.string().c_str()); } @@ -171,20 +172,22 @@ void PipelineCache::save(bool final) { auto synchronizationLock = synchronization.LockForSnapshot(); if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) return; } - if (liveBytes == settledBytes && readHeader() == settledFile) return; + const auto onDisk = readHeader(); + if (liveBytes == settledBytes && onDisk == settledFile) return; + const bool fileUnchanged = holdsFile && settledFile.has_value() && onDisk == settledFile; const auto started = std::chrono::steady_clock::now(); const auto settle = [&](const std::optional& file) { settledBytes = liveBytes; settledFile = file; }; - const auto disk = readFile(); + const auto disk = fileUnchanged ? File{} : readFile(); if (disk.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str()); settle(readHeader()); return; } if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason); - std::size_t floor = 0; + std::size_t floor = fileUnchanged ? std::max(loadedBytes, settledFile->dataBytes) : 0; VkPipelineCache merged = VK_NULL_HANDLE; struct DestroyMerged { const Context& context; @@ -231,7 +234,7 @@ void PipelineCache::save(bool final) { } if (size < floor) { std::fprintf(stderr, "[pipeline-cache] keeping %s: the merged cache has %.1f KiB, less than the %.1f KiB on disk or loaded at start\n", path.string().c_str(), static_cast(size) / 1024.0, static_cast(floor) / 1024.0); - settle(disk.header); + settle(fileUnchanged ? *settledFile : disk.header); return; } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; @@ -244,6 +247,10 @@ void PipelineCache::save(bool final) { if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast(size) / 1024.0, path.string().c_str(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), merged != VK_NULL_HANDLE ? " (merged with the file)" : "", final ? " (teardown)" : ""); } +void PipelineCache::SaveNow() { + save(false); +} + void PipelineCache::run() { std::unique_lock lock(mutex); while (!wake.wait_for(lock, std::chrono::seconds(SaveIntervalSeconds), [&] { return stopping; })) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp new file mode 100644 index 0000000000..77d4566132 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -0,0 +1,1141 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "ShaderCacheDirectory.hpp" +#include "ShaderDiskCache.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace AgcDriver::Graphics { + +namespace { + +static_assert(sizeof(VkPipelineColorBlendAttachmentState) == 32, "VkPipelineColorBlendAttachmentState changed: update the recipe codec"); +static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update the recipe codec"); +static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); +static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); +#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) +static_assert(sizeof(State) == 752, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); +static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); +static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); +#endif + +constexpr std::uint32_t FileMagic = 0x4c4a5041u; +// Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the +// layout guards above do not catch (a field renumbered or reinterpreted at the same size). +constexpr std::uint32_t FileFormat = 3u; +constexpr std::uint32_t ModuleRecord = 1u; +constexpr std::uint32_t GraphicsRecord = 2u; +constexpr std::uint32_t ComputeRecord = 3u; +constexpr std::uint64_t RecordAlignment = 8u; + +struct FileHeader { + std::uint32_t magic; + std::uint32_t format; + std::uint32_t runtimeAbi; + std::uint32_t reserved; + std::uint64_t sourceVersion; + std::uint64_t device; +}; +static_assert(sizeof(FileHeader) == 32 && std::is_trivially_copyable_v); + +struct RecordHeader { + std::uint32_t kind; + std::uint32_t reserved; + std::uint64_t payloadBytes; + std::uint64_t payloadHash; +}; +static_assert(sizeof(RecordHeader) == 24 && std::is_trivially_copyable_v); + +class Writer { +public: + explicit Writer(std::vector& out) : out(out) {} + + template + void Raw(const TValue& value) requires std::is_trivially_copyable_v { + const auto offset = out.size(); + out.resize(offset + sizeof(value)); + std::memcpy(out.data() + offset, &value, sizeof(value)); + } + + void Bool(bool value) { Raw(value ? 1u : 0u); } + void U32(std::uint32_t value) { Raw(value); } + void U64(std::uint64_t value) { Raw(value); } + + template + void Raws(std::span values) requires std::is_trivially_copyable_v { + U64(values.size()); + for (const auto& value : values) Raw(value); + } + +private: + std::vector& out; +}; + +class Reader { +public: + explicit Reader(std::span bytes) : bytes(bytes) {} + + [[nodiscard]] bool Ok() const { return ok; } + [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } + void Fail() { ok = false; } + + template + [[nodiscard]] TValue Raw() requires std::is_trivially_copyable_v { + TValue value{}; + if (!ok || bytes.size() - position < sizeof(value)) { + ok = false; + return value; + } + std::memcpy(&value, bytes.data() + position, sizeof(value)); + position += sizeof(value); + return value; + } + + [[nodiscard]] bool Bool() { + const auto value = Raw(); + if (value > 1u) ok = false; + return ok && value == 1u; + } + + [[nodiscard]] std::uint32_t U32() { return Raw(); } + [[nodiscard]] std::uint64_t U64() { return Raw(); } + + template + void Raws(std::vector& values, std::size_t limit) requires std::is_trivially_copyable_v { + const auto count = U64(); + if (!ok || count > limit || count > (bytes.size() - position) / sizeof(TValue)) { + ok = false; + values.clear(); + return; + } + values.resize(static_cast(count)); + for (auto& value : values) value = Raw(); + } + +private: + std::span bytes; + std::size_t position = 0; + bool ok = true; +}; + +template +void encodeOptional(Writer& writer, const std::optional& value, void (*encode)(Writer&, const TValue&)) { + writer.Bool(value.has_value()); + if (value) encode(writer, *value); +} + +void encodeMesh(Writer& writer, const ShaderRecompiler::MeshConfiguration& mesh) { + for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize, mesh.outputPrimitiveSize}) writer.U32(value); +} + +void decodeMesh(Reader& reader, ShaderRecompiler::MeshConfiguration& mesh) { + for (auto* value : {&mesh.inputPrimitive, &mesh.primitivesPerGroup, &mesh.verticesPerGroup, &mesh.maxVertices, &mesh.maxPrimitives, &mesh.threadsPerGroup, &mesh.ldsSizeDwords, &mesh.provokingVertex, &mesh.esgsItemSize, &mesh.outputPrimitiveSize}) *value = reader.U32(); +} + +void encodeTessellation(Writer& writer, const ShaderRecompiler::TessellationConfiguration& tessellation) { + for (const auto value : {tessellation.inputControlPoints, tessellation.outputControlPoints, tessellation.domain, tessellation.partitioning, tessellation.outputTopology, tessellation.patchesPerGroup, tessellation.primitiveGroupSize}) writer.U32(value); + writer.Bool(tessellation.breakAtInstance); + writer.U32(tessellation.instancePatches); + writer.Bool(tessellation.drawParameters); +} + +void decodeTessellation(Reader& reader, ShaderRecompiler::TessellationConfiguration& tessellation) { + for (auto* value : {&tessellation.inputControlPoints, &tessellation.outputControlPoints, &tessellation.domain, &tessellation.partitioning, &tessellation.outputTopology, &tessellation.patchesPerGroup, &tessellation.primitiveGroupSize}) *value = reader.U32(); + tessellation.breakAtInstance = reader.Bool(); + tessellation.instancePatches = reader.U32(); + tessellation.drawParameters = reader.Bool(); +} + +void encodeState(Writer& writer, const State& state) { + writer.U32(static_cast(state.stages.path)); + writer.U32(state.stages.vertexWaveSize); + writer.U32(state.stages.fragmentWaveSize); + encodeOptional(writer, state.stages.mesh, encodeMesh); + encodeOptional(writer, state.stages.tessellation, encodeTessellation); + writer.Bool(state.depth.has_value()); + if (state.depth) { + writer.U32(static_cast(state.depth->format)); + writer.U32(state.depth->samples); + } + writer.Bool(state.depthTest); + writer.Bool(state.depthWrite); + writer.U32(static_cast(state.depthCompare)); + writer.Bool(state.depthBoundsTest); + writer.Bool(state.depthBias); + writer.Raw(state.depthBiasClamp); + writer.Bool(state.depthBiasPerFace); + writer.Bool(state.stencilTest); + writer.Raw(state.stencilFront); + writer.Raw(state.stencilBack); + writer.Bool(state.stencilReplayReference.has_value()); + writer.U64(state.colors.size()); + for (const auto& color : state.colors) { + writer.U32(static_cast(color.format)); + writer.U32(color.exportIndex); + writer.Raw(color.componentMapping); + } + writer.Raws(std::span(state.blends)); + for (const auto value : state.blendConstants) writer.Raw(value); + writer.Bool(state.hasColorTarget); + writer.Bool(state.rectList); + writer.Bool(state.dualSourceBlend); + writer.U32(state.reversedBlend); + writer.U32(static_cast(state.topology)); + writer.Bool(state.primitiveRestart); + writer.Bool(state.negativeOneToOne); + writer.Bool(state.depthClamp); + writer.U32(static_cast(state.conservativeRasterization)); + writer.U32(state.cullMode); + writer.U32(static_cast(state.frontFace)); + writer.U32(static_cast(state.provokingVertexMode)); + writer.U32(state.samples); + for (const auto value : state.sampleLocations) writer.Raw(value); + writer.Bool(state.customSampleLocations); + writer.Bool(state.sampleLocationGrid); + writer.Bool(state.perSampleShading); + for (const auto mask : state.quadSampleMask) writer.Raw(mask); +} + +void decodeState(Reader& reader, State& state) { + state = State{}; + state.stages.path = static_cast(reader.U32()); + state.stages.vertexWaveSize = reader.U32(); + state.stages.fragmentWaveSize = reader.U32(); + if (reader.Bool()) decodeMesh(reader, state.stages.mesh.emplace()); + if (reader.Bool()) decodeTessellation(reader, state.stages.tessellation.emplace()); + if (reader.Bool()) { + auto& depth = state.depth.emplace(); + depth.format = static_cast(reader.U32()); + depth.samples = reader.U32(); + } + state.depthTest = reader.Bool(); + state.depthWrite = reader.Bool(); + state.depthCompare = static_cast(reader.U32()); + state.depthBoundsTest = reader.Bool(); + state.depthBias = reader.Bool(); + state.depthBiasClamp = reader.Raw(); + state.depthBiasPerFace = reader.Bool(); + state.stencilTest = reader.Bool(); + state.stencilFront = reader.Raw(); + state.stencilBack = reader.Raw(); + if (reader.Bool()) state.stencilReplayReference = 0u; + const auto colors = reader.U64(); + if (!reader.Ok() || colors > 8u) { + reader.Fail(); + state = State{}; + return; + } + for (std::uint64_t index = 0; index < colors; ++index) { + ColorTarget color{}; + color.format = static_cast(reader.U32()); + color.exportIndex = reader.U32(); + color.componentMapping = reader.Raw(); + state.colors.push_back(color); + } + reader.Raws(state.blends, 8u); + for (auto& value : state.blendConstants) value = reader.Raw(); + state.hasColorTarget = reader.Bool(); + state.rectList = reader.Bool(); + state.dualSourceBlend = reader.Bool(); + state.reversedBlend = reader.U32(); + state.topology = static_cast(reader.U32()); + state.primitiveRestart = reader.Bool(); + state.negativeOneToOne = reader.Bool(); + state.depthClamp = reader.Bool(); + state.conservativeRasterization = static_cast(reader.U32()); + state.cullMode = reader.U32(); + state.frontFace = static_cast(reader.U32()); + state.provokingVertexMode = static_cast(reader.U32()); + state.samples = reader.U32(); + for (auto& value : state.sampleLocations) value = reader.Raw(); + state.customSampleLocations = reader.Bool(); + state.sampleLocationGrid = reader.Bool(); + state.perSampleShading = reader.Bool(); + for (auto& mask : state.quadSampleMask) mask = reader.Raw(); +} + +void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { + writer.U32(static_cast(recipe.attachmentLayout)); + writer.U64(recipe.stages.size()); + for (const auto& stage : recipe.stages) { + writer.U32(static_cast(stage.stage)); + writer.U64(stage.identity.low); + writer.U64(stage.identity.high); + writer.U32(stage.pushConstantOffset); + writer.Bool(stage.generated); + } + writer.Raws(std::span(recipe.vertexInput.bindings)); + writer.Raws(std::span(recipe.vertexInput.attributes)); + writer.Raws(std::span(recipe.mainLayoutKey)); + writer.Raws(std::span(recipe.fragmentLayoutKey)); + writer.Bool(recipe.splitSets); + encodeState(writer, recipe.state); +} + +bool decodeRecipe(Reader& reader, GraphicsPipelineRecipe& recipe) { + recipe = GraphicsPipelineRecipe{}; + recipe.attachmentLayout = static_cast(reader.U32()); + const auto stages = reader.U64(); + if (!reader.Ok() || stages > 8u) return false; + for (std::uint64_t index = 0; index < stages; ++index) { + GraphicsPipelineRecipe::Stage stage{}; + stage.stage = static_cast(reader.U32()); + stage.identity.low = reader.U64(); + stage.identity.high = reader.U64(); + stage.pushConstantOffset = reader.U32(); + stage.generated = reader.Bool(); + recipe.stages.push_back(stage); + } + reader.Raws(recipe.vertexInput.bindings, 64u); + reader.Raws(recipe.vertexInput.attributes, 64u); + reader.Raws(recipe.mainLayoutKey, 4096u); + reader.Raws(recipe.fragmentLayoutKey, 4096u); + recipe.splitSets = reader.Bool(); + decodeState(reader, recipe.state); + return reader.Done(); +} + +std::uint64_t padded(std::uint64_t bytes) { + return (bytes + RecordAlignment - 1u) / RecordAlignment * RecordAlignment; +} + +bool profiling() { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + return profile; +} + +thread_local bool replayingHere = false; + +std::uint64_t configured(const char* name, std::uint64_t fallback) { + const char* text = std::getenv(name); + if (text == nullptr || *text == '\0') return fallback; + char* end = nullptr; + const auto value = std::strtoull(text, &end, 10); + if (end == text || *end != '\0') throw std::runtime_error(std::string("AGC graphics: ") + name + " must be a decimal integer"); + return value; +} + +// Held while the journal is read, cut or appended: another process sharing the directory (a +// second run of the same install) takes the same lock file, so its appends and this run's never +// interleave and neither cuts a record the other is writing. Taken before the journal's mutex, +// never while holding it. +class FileLock { +public: + explicit FileLock(const std::filesystem::path& path) { +#ifdef _WIN32 + handle = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) return; + OVERLAPPED overlapped{}; + if (!LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &overlapped)) { + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + } +#else + descriptor = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644); + if (descriptor >= 0 && ::flock(descriptor, LOCK_EX) != 0) { + ::close(descriptor); + descriptor = -1; + } +#endif + } + + ~FileLock() { +#ifdef _WIN32 + if (handle == INVALID_HANDLE_VALUE) return; + OVERLAPPED overlapped{}; + UnlockFileEx(handle, 0, 1, 0, &overlapped); + CloseHandle(handle); +#else + if (descriptor < 0) return; + ::flock(descriptor, LOCK_UN); + ::close(descriptor); +#endif + } + + FileLock(const FileLock&) = delete; + FileLock& operator=(const FileLock&) = delete; + +#ifdef _WIN32 + bool Held() const { return handle != INVALID_HANDLE_VALUE; } +#else + bool Held() const { return descriptor >= 0; } +#endif + +private: +#ifdef _WIN32 + HANDLE handle = INVALID_HANDLE_VALUE; +#else + int descriptor = -1; +#endif +}; + +// The end of the last whole record in [from, size) of the file: records another process appended +// after this one loaded the file are kept, the torn tail of a record a crashed run left is cut. +std::uint64_t validTail(const std::filesystem::path& path, std::uint64_t from, std::uint64_t size); + +struct LoadedModule { + ShaderRecompiler::ShaderStage stage; + std::shared_ptr result; +}; + +struct LoadedEntry { + std::uint32_t kind; + GraphicsPipelineRecipe graphics; + ShaderRecompiler::PipelineIdentity compute; + bool pushConstants = false; +}; + +struct LoadedJournal { + std::map modules; + std::vector entries; + std::set indexModules; + std::set indexRecords; + std::uint64_t validBytes = 0; + std::uint64_t bytes = 0; + std::uint64_t dropped = 0; + bool headerValid = false; +}; + +class Journal { +public: + static Journal& Instance() { + static auto* journal = Create(); + return *journal; + } + + static Journal* Existing() { return created().load(std::memory_order_acquire); } + + [[nodiscard]] bool Enabled() const { return !path.empty() && !mismatched.load(std::memory_order_relaxed); } + [[nodiscard]] const std::filesystem::path& Path() const { return path; } + + // The journal belongs to one device per process: the first device to replay binds its + // fingerprint, the file header names it and a file written by another device starts over. A + // later device with another fingerprint (the window's device is the headless device's GPU, so a + // run does not see one) turns the journal off for the rest of the run. + bool Bind(std::uint64_t fingerprint) { + std::lock_guard lock(mutex); + if (!bound.load(std::memory_order_relaxed)) { + device.store(fingerprint, std::memory_order_relaxed); + bound.store(true, std::memory_order_release); + return true; + } + if (device.load(std::memory_order_relaxed) == fingerprint) return true; + if (!mismatched.exchange(true)) std::fprintf(stderr, "[pipeline-journal] %s belongs to another device; the journal is off for this run\n", path.string().c_str()); + return false; + } + + // Reads the whole file (modules keep views into the buffer, which lives as long as they do), + // fills the dedupe index, and remembers how many bytes parsed so a torn tail is cut before the + // next append. + LoadedJournal Load() { + LoadedJournal loaded; + if (!Enabled() || !bound.load(std::memory_order_acquire)) return loaded; + const auto started = std::chrono::steady_clock::now(); + auto file = std::make_shared>(); + bool read = false; + { + FileLock fileLock(lockPath); + read = ShaderRecompiler::ReadWholeFile(path, *file); + } + if (read) parse(*file, file, loaded, true); + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + { + std::lock_guard lock(mutex); + std::error_code error; + if (!indexed && (read || !std::filesystem::exists(path, error))) { + indexed = true; + validBytes = loaded.validBytes; + headerValid = loaded.headerValid; + writtenModules = loaded.indexModules; + writtenRecords = loaded.indexRecords; + } + counters.loadedRecipes.store(loaded.entries.size(), std::memory_order_relaxed); + counters.loadedModules.store(loaded.modules.size(), std::memory_order_relaxed); + counters.loadedBytes.store(loaded.bytes, std::memory_order_relaxed); + counters.droppedRecords.store(loaded.dropped, std::memory_order_relaxed); + loadMs.store(ms, std::memory_order_relaxed); + } + if (loaded.bytes != 0 || loaded.dropped != 0) std::fprintf(stderr, "[pipeline-journal] loaded %zu pipelines and %zu modules (%.1f MiB) from %s in %.0f ms%s\n", loaded.entries.size(), loaded.modules.size(), static_cast(loaded.bytes) / (1024.0 * 1024.0), path.string().c_str(), ms, !loaded.headerValid ? "; written by another build or device, starting over" : loaded.dropped != 0 ? "; the file's tail was unreadable and is dropped" : ""); + return loaded; + } + + void RecordGraphics(const GraphicsPipelineRecipe& recipe, std::span shaders) { + if (!Enabled() || !bound.load(std::memory_order_acquire)) return; + Pending pending; + pending.kind = GraphicsRecord; + for (std::size_t index = 0; index < shaders.size() && index < recipe.stages.size(); ++index) { + Require(shaders[index].program != nullptr, "a recorded pipeline stage has no program"); + pending.modules.push_back({recipe.stages[index].identity, recipe.stages[index].stage, std::make_shared(*shaders[index].program)}); + } + Writer writer(pending.payload); + encodeRecipe(writer, recipe); + enqueue(std::move(pending)); + } + + void RecordCompute(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + if (!Enabled() || !bound.load(std::memory_order_acquire)) return; + const auto identity = shader.Identity(); + if (identity.Empty()) return; + Pending pending; + pending.kind = ComputeRecord; + pending.modules.push_back({identity, ShaderRecompiler::ShaderStage::Compute, std::make_shared(shader)}); + Writer writer(pending.payload); + writer.U64(identity.low); + writer.U64(identity.high); + writer.Bool(pushConstants); + enqueue(std::move(pending)); + } + + void Flush() { + std::unique_lock lock(mutex); + idle.wait(lock, [&] { return queue.empty() && !writing; }); + } + + PipelineJournalCounters Totals() const { + PipelineJournalCounters totals; + totals.loadedRecipes = counters.loadedRecipes.load(std::memory_order_relaxed); + totals.loadedModules = counters.loadedModules.load(std::memory_order_relaxed); + totals.loadedBytes = counters.loadedBytes.load(std::memory_order_relaxed); + totals.droppedRecords = counters.droppedRecords.load(std::memory_order_relaxed); + totals.loadMs = loadMs.load(std::memory_order_relaxed); + totals.recordedGraphics = counters.recordedGraphics.load(std::memory_order_relaxed); + totals.recordedCompute = counters.recordedCompute.load(std::memory_order_relaxed); + totals.recordedModules = counters.recordedModules.load(std::memory_order_relaxed); + totals.writtenBytes = counters.writtenBytes.load(std::memory_order_relaxed); + totals.writeFailures = counters.writeFailures.load(std::memory_order_relaxed); + return totals; + } + + struct Counters { + std::atomic loadedRecipes{0}, loadedModules{0}, loadedBytes{0}, droppedRecords{0}; + std::atomic recordedGraphics{0}, recordedCompute{0}, recordedModules{0}, writtenBytes{0}, writeFailures{0}; + }; + +private: + struct PendingModule { + ShaderRecompiler::PipelineIdentity identity; + ShaderRecompiler::ShaderStage stage; + std::shared_ptr result; + }; + + struct Pending { + std::uint32_t kind = 0; + std::vector modules; + std::vector payload; + }; + + static std::atomic& created() { + static std::atomic pointer{nullptr}; + return pointer; + } + + static Journal* Create() { + auto* journal = new Journal(); + created().store(journal, std::memory_order_release); + return journal; + } + + Journal() { + const char* off = std::getenv("APS5_NO_PIPELINE_JOURNAL"); + if ((off != nullptr && std::strcmp(off, "0") != 0) || !ShaderRecompiler::ShaderDiskCache::Enabled()) return; + path = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal"; + lockPath = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal.lock"; + maxBytes = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; + std::thread([this] { run(); }).detach(); + } + + // Parses the file: with `loaded` set every record is decoded, otherwise only the index (the + // module identities and record hashes) is built. Stops at the first record that does not + // parse; the bytes before it stay valid. + void parse(const std::vector& file, const std::shared_ptr>& owner, LoadedJournal& loaded, bool decode) { + std::uint64_t valid = 0; + bool header = false; + std::uint64_t dropped = 0; + const auto bytes = std::span(file); + if (bytes.size() >= sizeof(FileHeader)) { + FileHeader fileHeader; + std::memcpy(&fileHeader, bytes.data(), sizeof(fileHeader)); + header = fileHeader.magic == FileMagic && fileHeader.format == FileFormat && fileHeader.runtimeAbi == ShaderRecompiler::RuntimeAbi::Version && fileHeader.sourceVersion == ShaderRecompiler::ShaderDiskCache::SourceVersion() && fileHeader.device == device.load(std::memory_order_relaxed); + } + if (header) { + valid = padded(sizeof(FileHeader)); + std::uint64_t cursor = valid; + while (cursor + sizeof(RecordHeader) <= bytes.size()) { + RecordHeader record; + std::memcpy(&record, bytes.data() + cursor, sizeof(record)); + const auto payloadOffset = cursor + sizeof(RecordHeader); + if (record.payloadBytes > bytes.size() - payloadOffset) break; + const auto end = padded(payloadOffset + record.payloadBytes); + if (end > bytes.size()) break; + const auto payload = bytes.subspan(static_cast(payloadOffset), static_cast(record.payloadBytes)); + if (ShaderRecompiler::HashBytes(payload) != record.payloadHash) break; + if (record.kind == ModuleRecord) { + if (payload.size() < 20u) break; + ShaderRecompiler::PipelineIdentity identity; + std::memcpy(&identity.low, payload.data(), sizeof(identity.low)); + std::memcpy(&identity.high, payload.data() + 8, sizeof(identity.high)); + std::uint32_t stage = 0; + std::memcpy(&stage, payload.data() + 16, sizeof(stage)); + bool decoded = !decode; + if (decode && !loaded.indexModules.contains(identity)) { + auto result = std::make_shared(); + decoded = ShaderRecompiler::ShaderDiskCache::DecodeResult(payload.subspan(20), *result, owner) && result->Identity() == identity; + if (decoded) loaded.modules.emplace(identity, LoadedModule{static_cast(stage), std::move(result)}); + else ++dropped; + } + if (decoded) loaded.indexModules.insert(identity); + } else if (record.kind == GraphicsRecord || record.kind == ComputeRecord) { + if (decode && !loaded.indexRecords.contains(record.payloadHash)) { + LoadedEntry entry; + entry.kind = record.kind; + bool decoded = false; + if (record.kind == GraphicsRecord) { + Reader reader(payload); + decoded = decodeRecipe(reader, entry.graphics); + } else { + Reader reader(payload); + entry.compute.low = reader.U64(); + entry.compute.high = reader.U64(); + entry.pushConstants = reader.Bool(); + decoded = reader.Done(); + } + if (decoded) loaded.entries.push_back(std::move(entry)); + else ++dropped; + } + loaded.indexRecords.insert(record.payloadHash); + } else { + break; + } + valid = end; + cursor = end; + } + } + if (valid < bytes.size()) ++dropped; + loaded.validBytes = valid; + loaded.bytes = bytes.size(); + loaded.dropped = dropped; + loaded.headerValid = header; + } + + // The index the writer dedupes against, replaced from the file: what it holds and where it + // ends. Under the file lock. False when the file exists but could not be read, so nothing is + // written until it can be (the next record tries again) and the file is never started over for + // a read failure. + bool indexLocked() { + std::vector file; + LoadedJournal index; + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (exists && !ShaderRecompiler::ReadWholeFile(path, file)) return false; + if (exists) parse(file, nullptr, index, false); + std::lock_guard lock(mutex); + indexed = true; + validBytes = index.validBytes; + headerValid = index.headerValid; + writtenModules = std::move(index.indexModules); + writtenRecords = std::move(index.indexRecords); + return true; + } + + bool ensureIndexed() { + { + std::lock_guard lock(mutex); + if (indexed) return true; + } + FileLock fileLock(lockPath); + { + std::lock_guard lock(mutex); + if (indexed) return true; + } + return indexLocked(); + } + + void enqueue(Pending pending) { + { + std::lock_guard lock(mutex); + queue.push_back(std::move(pending)); + } + wake.notify_one(); + } + + void run() { + for (;;) { + Pending pending; + { + std::unique_lock lock(mutex); + wake.wait(lock, [&] { return !queue.empty(); }); + pending = std::move(queue.front()); + queue.pop_front(); + writing = true; + } + try { + write(pending); + } catch (const std::exception& error) { + counters.writeFailures.fetch_add(1, std::memory_order_relaxed); + static std::atomic reported{0}; + if (reported.fetch_add(1) < 4) std::fprintf(stderr, "[pipeline-journal] cannot write %s: %s\n", path.string().c_str(), error.what()); + } + { + std::lock_guard lock(mutex); + writing = false; + if (queue.empty()) idle.notify_all(); + } + } + } + + // One record per module the file lacks, then the pipeline record; a record whose payload the + // file already holds is skipped. The file is opened for appending per batch, after the torn + // tail of a crashed run (bytes past the last parsed record) was cut. + void write(const Pending& pending) { + if (!ensureIndexed()) throw std::runtime_error("the journal could not be read"); + for (unsigned attempt = 0; attempt < 2; ++attempt) { + if (append(pending)) return; + } + throw std::runtime_error("the journal keeps changing under this run"); + } + + // One attempt: false when the file was cut or replaced by someone else since it was indexed + // (it is indexed again, and the batch is rebuilt against that index). + bool append(const Pending& pending) { + const auto recordHash = ShaderRecompiler::HashBytes(pending.payload); + std::vector batch; + std::vector newModules; + { + std::lock_guard lock(mutex); + if (writtenRecords.contains(recordHash)) return true; + for (const auto& module : pending.modules) { + if (!writtenModules.contains(module.identity) && std::ranges::find(newModules, module.identity) == newModules.end()) newModules.push_back(module.identity); + } + } + const auto appendRecord = [&](std::uint32_t kind, std::span payload) { + const RecordHeader header{kind, 0u, payload.size(), ShaderRecompiler::HashBytes(payload)}; + Writer writer(batch); + writer.Raw(header); + batch.insert(batch.end(), payload.begin(), payload.end()); + batch.resize(static_cast(padded(batch.size()))); + }; + for (const auto& identity : newModules) { + const auto module = std::ranges::find_if(pending.modules, [&](const PendingModule& candidate) { return candidate.identity == identity; }); + std::vector payload; + Writer writer(payload); + writer.U64(identity.low); + writer.U64(identity.high); + writer.U32(static_cast(module->stage)); + ShaderRecompiler::ShaderDiskCache::EncodeResult(*module->result, payload); + appendRecord(ModuleRecord, payload); + } + appendRecord(pending.kind, pending.payload); + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + FileLock fileLock(lockPath); + if (!fileLock.Held()) { + static std::atomic reported{false}; + if (!reported.exchange(true)) std::fprintf(stderr, "[pipeline-journal] cannot lock %s; pipelines are not recorded while it cannot be locked\n", lockPath.string().c_str()); + counters.writeFailures.fetch_add(1, std::memory_order_relaxed); + return true; + } + bool fresh = false; + { + std::lock_guard lock(mutex); + fresh = !headerValid; + if (!fresh) { + const auto size = std::filesystem::file_size(path, error); + if (error) fresh = true; + else if (size > validBytes) { + const auto tail = validTail(path, validBytes, size); + if (tail < size) std::filesystem::resize_file(path, tail, error); + if (error) fresh = true; + else validBytes = tail; + } + } + if (!fresh && validBytes + batch.size() > maxBytes) { + static std::atomic reported{false}; + if (!reported.exchange(true)) std::fprintf(stderr, "[pipeline-journal] %s reached APS5_PIPELINE_JOURNAL_MAX_MIB; later pipelines are not recorded\n", path.string().c_str()); + counters.writeFailures.fetch_add(1, std::memory_order_relaxed); + return true; + } + } + if (!fresh) { + const auto size = std::filesystem::file_size(path, error); + if (!error && size < validBytes) { + if (!indexLocked()) throw std::runtime_error("the journal could not be read"); + return false; + } + } + std::vector prefix; + if (fresh) { + const FileHeader header{FileMagic, FileFormat, ShaderRecompiler::RuntimeAbi::Version, 0u, ShaderRecompiler::ShaderDiskCache::SourceVersion(), device.load(std::memory_order_relaxed)}; + Writer writer(prefix); + writer.Raw(header); + prefix.resize(static_cast(padded(prefix.size()))); + } + std::ofstream file(path, fresh ? std::ios::binary | std::ios::trunc : std::ios::binary | std::ios::app); + if (!file) throw std::runtime_error("cannot open the journal for writing"); + if (!prefix.empty()) file.write(reinterpret_cast(prefix.data()), static_cast(prefix.size())); + file.write(reinterpret_cast(batch.data()), static_cast(batch.size())); + file.flush(); + if (file.fail()) throw std::runtime_error("the journal write failed"); + std::lock_guard lock(mutex); + headerValid = true; + validBytes = (fresh ? padded(sizeof(FileHeader)) : validBytes) + batch.size(); + for (const auto& identity : newModules) writtenModules.insert(identity); + writtenRecords.insert(recordHash); + counters.recordedModules.fetch_add(newModules.size(), std::memory_order_relaxed); + if (pending.kind == GraphicsRecord) counters.recordedGraphics.fetch_add(1, std::memory_order_relaxed); + else counters.recordedCompute.fetch_add(1, std::memory_order_relaxed); + counters.writtenBytes.fetch_add(prefix.size() + batch.size(), std::memory_order_relaxed); + return true; + } + + std::filesystem::path path; + std::filesystem::path lockPath; + std::uint64_t maxBytes = 0; + std::atomic device{0}; + std::atomic bound{false}; + std::atomic mismatched{false}; + std::mutex mutex; + std::condition_variable wake; + std::condition_variable idle; + std::deque queue; + bool writing = false; + bool indexed = false; + bool headerValid = false; + std::uint64_t validBytes = 0; + std::set writtenModules; + std::set writtenRecords; + Counters counters; + std::atomic loadMs{0}; +}; + +std::uint64_t validTail(const std::filesystem::path& path, std::uint64_t from, std::uint64_t size) { + if (size <= from) return from; + std::ifstream file(path, std::ios::binary); + if (!file) return from; + file.seekg(static_cast(from)); + std::vector bytes(static_cast(size - from)); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(file.gcount()) != bytes.size()) return from; + const auto span = std::span(bytes); + std::uint64_t cursor = 0; + std::uint64_t valid = 0; + while (cursor + sizeof(RecordHeader) <= span.size()) { + RecordHeader record; + std::memcpy(&record, span.data() + cursor, sizeof(record)); + const auto payloadOffset = cursor + sizeof(RecordHeader); + if (record.kind == 0 || record.kind > ComputeRecord || record.payloadBytes > span.size() - payloadOffset) break; + const auto end = padded(payloadOffset + record.payloadBytes); + if (end > span.size()) break; + if (ShaderRecompiler::HashBytes(span.subspan(static_cast(payloadOffset), static_cast(record.payloadBytes))) != record.payloadHash) break; + valid = end; + cursor = end; + } + return from + valid; +} + +struct ReplayCounters { + std::atomic graphics{0}, compute{0}, present{0}, skipped{0}, rejected{0}, failed{0}; + std::atomic threads{0}; + std::atomic ms{0}; +}; + +ReplayCounters& replayCounters() { + static ReplayCounters counters; + return counters; +} + +// Builds the journal's pipelines for one device on a pool: the first thread loads the file, every +// thread then takes the next entry. A pipeline the store already holds counts as present; an entry +// whose modules are missing or whose set layouts differ from what its modules describe is rejected. +class Replay : public PipelineJournalReplay { +public: + // The context keeps the device's buffer pool: a store entry is alive for a draw only while its + // pool is the draw's (the pool tells device instances apart), so the entries built here must + // name it. The pool is released with the replay, before the device's teardown resets it. + Replay(Context context, PipelineJournalHooks hooks, unsigned threads) : context(std::move(context)), hooks(std::move(hooks)), active(threads) { + started = std::chrono::steady_clock::now(); + replayCounters().threads.store(threads, std::memory_order_relaxed); + workers.reserve(threads); + for (unsigned index = 0; index < threads; ++index) workers.emplace_back([this] { run(); }); + } + + ~Replay() override { + Cancel(); + Wait(); + } + + void Cancel() override { cancelled.store(true, std::memory_order_relaxed); } + + void Wait() override { + for (auto& worker : workers) { + if (worker.joinable()) worker.join(); + } + workers.clear(); + } + + [[nodiscard]] bool Done() const override { return finished.load(std::memory_order_acquire); } + +private: + void run() { + std::call_once(loadOnce, [&] { + try { + auto journal = Journal::Instance().Load(); + const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 1024u); + if (journal.entries.size() > limit) journal.entries.resize(static_cast(limit)); + loaded = std::move(journal); + } catch (const std::exception& error) { + std::fprintf(stderr, "[pipeline-journal] cannot load %s: %s\n", Journal::Instance().Path().string().c_str(), error.what()); + } + }); + for (;;) { + if (cancelled.load(std::memory_order_relaxed)) break; + const auto index = cursor.fetch_add(1, std::memory_order_relaxed); + if (index >= loaded.entries.size()) break; + try { + replay(loaded.entries[index]); + } catch (const std::exception& error) { + replayCounters().failed.fetch_add(1, std::memory_order_relaxed); + static std::atomic reported{0}; + if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[pipeline-journal] replay failed: %s\n", error.what()); + } + } + if (active.fetch_sub(1, std::memory_order_acq_rel) == 1) { + loaded = LoadedJournal{}; + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + replayCounters().ms.store(ms, std::memory_order_relaxed); + finished.store(true, std::memory_order_release); + ReportPipelineJournal(); + } + } + + void replay(const LoadedEntry& entry) { + struct Replaying { + Replaying() { replayingHere = true; } + ~Replaying() { replayingHere = false; } + } replaying; + auto& counters = replayCounters(); + if (entry.kind == ComputeRecord) { + const auto module = loaded.modules.find(entry.compute); + if (module == loaded.modules.end() || module->second.stage != ShaderRecompiler::ShaderStage::Compute || !hooks.compute) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + (hooks.compute(*module->second.result, entry.pushConstants) ? counters.compute : counters.present).fetch_add(1, std::memory_order_relaxed); + return; + } + std::vector shaders; + for (const auto& stage : entry.graphics.stages) { + const auto module = loaded.modules.find(stage.identity); + if (module == loaded.modules.end() || module->second.stage != stage.stage) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + shaders.push_back({stage.stage, module->second.result.get(), stage.pushConstantOffset}); + } + if (shaders.empty() || context.descriptorCache == nullptr) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + const auto store = PipelineStoreTotals(); + if (store.cached >= store.bound) { + counters.skipped.fetch_add(1, std::memory_order_relaxed); + return; + } + const auto layouts = DescribeSetLayouts(context, shaders); + if (!std::ranges::equal(layouts.mainKey, entry.graphics.mainLayoutKey) || !std::ranges::equal(layouts.fragmentKey, entry.graphics.fragmentLayoutKey) || layouts.layouts.split != entry.graphics.splitSets) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + bool created = false; + static_cast(CachedPipeline(context, entry.graphics.state, entry.graphics.vertexInput, layouts.View(), shaders, entry.graphics.attachmentLayout, &created)); + (created ? counters.graphics : counters.present).fetch_add(1, std::memory_order_relaxed); + } + + Context context; + PipelineJournalHooks hooks; + std::vector workers; + std::once_flag loadOnce; + LoadedJournal loaded; + std::atomic cursor{0}; + std::atomic active{0}; + std::atomic finished{false}; + std::atomic cancelled{false}; + std::chrono::steady_clock::time_point started; +}; + +} + +State CanonicalPipelineState(const State& state) { + State canonical{}; + canonical.stages.path = state.stages.path; + canonical.stages.vertexWaveSize = state.stages.vertexWaveSize; + canonical.stages.fragmentWaveSize = state.stages.fragmentWaveSize; + canonical.stages.mesh = state.stages.mesh; + canonical.stages.tessellation = state.stages.tessellation; + if (state.depth) { + DepthTarget depth{}; + depth.format = state.depth->format; + depth.samples = state.depth->samples; + canonical.depth = depth; + } + canonical.depthTest = state.depthTest; + canonical.depthWrite = state.depthWrite; + canonical.depthCompare = state.depthCompare; + canonical.depthBoundsTest = state.depthBoundsTest; + canonical.depthBias = state.depthBias; + canonical.depthBiasClamp = state.depthBiasClamp; + canonical.depthBiasPerFace = state.depthBiasPerFace; + canonical.stencilTest = state.stencilTest; + canonical.stencilFront = state.stencilFront; + canonical.stencilBack = state.stencilBack; + if (state.stencilReplayReference) canonical.stencilReplayReference = 0u; + for (const auto& color : state.colors) { + ColorTarget canonicalColor{}; + canonicalColor.format = color.format; + canonicalColor.exportIndex = color.exportIndex; + canonicalColor.componentMapping = color.componentMapping; + canonical.colors.push_back(canonicalColor); + } + canonical.blends = state.blends; + canonical.blendConstants = state.blendConstants; + canonical.hasColorTarget = state.hasColorTarget; + canonical.rectList = state.rectList; + canonical.dualSourceBlend = state.dualSourceBlend; + canonical.reversedBlend = state.reversedBlend; + canonical.topology = state.topology; + canonical.primitiveRestart = state.primitiveRestart; + canonical.negativeOneToOne = state.negativeOneToOne; + canonical.depthClamp = state.depthClamp; + canonical.conservativeRasterization = state.conservativeRasterization; + canonical.cullMode = state.cullMode; + canonical.frontFace = state.frontFace; + canonical.provokingVertexMode = state.provokingVertexMode; + canonical.samples = state.samples; + canonical.sampleLocations = state.sampleLocations; + canonical.customSampleLocations = state.customSampleLocations; + canonical.sampleLocationGrid = state.sampleLocationGrid; + canonical.perSampleShading = state.perSampleShading; + canonical.quadSampleMask = state.quadSampleMask; + return canonical; +} + +bool GeneratedPipelineStage(const State& state, ShaderRecompiler::ShaderStage stage) { + using Stage = ShaderRecompiler::ShaderStage; + return (state.rectList && (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation)) || stage == Stage::Geometry; +} + +GraphicsPipelineRecipe MakeGraphicsPipelineRecipe(const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { + GraphicsPipelineRecipe recipe; + recipe.attachmentLayout = attachmentLayout; + for (const auto& shader : shaders) { + Require(shader.program != nullptr, "missing compiled shader"); + recipe.stages.push_back({shader.stage, shader.program->Identity(), shader.pushConstantOffset, GeneratedPipelineStage(state, shader.stage)}); + } + recipe.vertexInput = vertexInput; + recipe.mainLayoutKey.assign(resources.mainKey.begin(), resources.mainKey.end()); + recipe.fragmentLayoutKey.assign(resources.fragmentKey.begin(), resources.fragmentKey.end()); + recipe.splitSets = resources.split; + recipe.state = CanonicalPipelineState(state); + return recipe; +} + +std::vector EncodeGraphicsPipelineRecipe(const GraphicsPipelineRecipe& recipe) { + std::vector bytes; + Writer writer(bytes); + encodeRecipe(writer, recipe); + return bytes; +} + +bool DecodeGraphicsPipelineRecipe(std::span bytes, GraphicsPipelineRecipe& recipe) { + Reader reader(bytes); + return decodeRecipe(reader, recipe); +} + +std::uint64_t PipelineSwitch(const char* name, std::uint64_t fallback) { + return configured(name, fallback); +} + +bool PipelineJournalEnabled() { + return Journal::Instance().Enabled(); +} + +bool PipelineJournalReplaying() { + return replayingHere; +} + +std::filesystem::path PipelineJournalPath() { + return Journal::Instance().Path(); +} + +PipelineJournalCounters PipelineJournalTotals() { + auto totals = Journal::Instance().Totals(); + const auto& replay = replayCounters(); + totals.replayedGraphics = replay.graphics.load(std::memory_order_relaxed); + totals.replayedCompute = replay.compute.load(std::memory_order_relaxed); + totals.replayPresent = replay.present.load(std::memory_order_relaxed); + totals.replaySkipped = replay.skipped.load(std::memory_order_relaxed); + totals.replayRejected = replay.rejected.load(std::memory_order_relaxed); + totals.replayFailed = replay.failed.load(std::memory_order_relaxed); + totals.replayMs = replay.ms.load(std::memory_order_relaxed); + totals.replayThreads = replay.threads.load(std::memory_order_relaxed); + return totals; +} + +void RecordGraphicsPipeline(const GraphicsPipelineRecipe& recipe, std::span shaders) { + Journal::Instance().RecordGraphics(recipe, shaders); +} + +void RecordComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + Journal::Instance().RecordCompute(shader, pushConstants); +} + +void FlushPipelineJournal() { + auto* journal = Journal::Existing(); + if (journal != nullptr && journal->Enabled()) journal->Flush(); +} + +void ReportPipelineJournal() { + if (!profiling() || !Journal::Instance().Enabled()) return; + const auto totals = PipelineJournalTotals(); + AgcDriver::ProfilePrint_nid_no_patch("[pipeline-journal] loaded %llu pipelines, %llu modules (%.1f MiB, %.0f ms); replay on %u threads: %llu graphics and %llu compute built, %llu present, %llu skipped (store full), %llu rejected, %llu failed in %.0f ms; recorded this run: %llu graphics, %llu compute, %llu modules (%.1f MiB), %llu write failures\n", + static_cast(totals.loadedRecipes), static_cast(totals.loadedModules), static_cast(totals.loadedBytes) / (1024.0 * 1024.0), totals.loadMs, totals.replayThreads, + static_cast(totals.replayedGraphics), static_cast(totals.replayedCompute), static_cast(totals.replayPresent), static_cast(totals.replaySkipped), static_cast(totals.replayRejected), static_cast(totals.replayFailed), totals.replayMs, + static_cast(totals.recordedGraphics), static_cast(totals.recordedCompute), static_cast(totals.recordedModules), static_cast(totals.writtenBytes) / (1024.0 * 1024.0), static_cast(totals.writeFailures)); +} + +std::uint64_t PipelineJournalDeviceFingerprint(const VkPhysicalDeviceProperties& properties) { + std::array words{properties.vendorID, properties.deviceID, properties.driverVersion, 0u, 0u, 0u, 0u}; + static_assert(VK_UUID_SIZE == 16); + std::memcpy(words.data() + 3, properties.pipelineCacheUUID, VK_UUID_SIZE); + return ShaderRecompiler::HashSpirvWords(words, 0x6a6f75726e616c01ull); +} + +std::shared_ptr StartPipelineJournalReplay(const Context& context, std::uint64_t deviceFingerprint, PipelineJournalHooks hooks) { + if (!Journal::Instance().Enabled() || !Journal::Instance().Bind(deviceFingerprint)) return nullptr; + const auto threads = static_cast(configured("APS5_PIPELINE_JOURNAL_THREADS", std::max(1u, std::thread::hardware_concurrency() / 2u))); + if (threads == 0) return nullptr; + return std::make_shared(context, std::move(hooks), threads); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index bd2a12ed32..7670cde694 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -1,6 +1,8 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include +#include #include #include #include @@ -52,9 +54,13 @@ struct LibraryStore { std::condition_variable work; std::deque>> jobs; std::map pending; - bool started = false; + std::map activeBuilds; + std::thread worker; + bool stopping = false; }; +std::atomic runningWorkers{0}; + LibraryStore& Libraries() { static auto* store = new LibraryStore(); return *store; @@ -63,7 +69,8 @@ LibraryStore& Libraries() { void runJobs(LibraryStore& store) { std::unique_lock lock(store.mutex); for (;;) { - store.work.wait(lock, [&] { return !store.jobs.empty(); }); + store.work.wait(lock, [&] { return !store.jobs.empty() || store.stopping; }); + if (store.stopping) break; auto [device, job] = std::move(store.jobs.front()); store.jobs.pop_front(); lock.unlock(); @@ -71,15 +78,18 @@ void runJobs(LibraryStore& store) { lock.lock(); if (--store.pending[device] == 0) store.idle.notify_all(); } + runningWorkers.fetch_sub(1, std::memory_order_acq_rel); } void enqueue(LibraryStore& store, VkDevice device, std::function job) { - if (!store.started) { - std::thread(runJobs, std::ref(store)).detach(); - store.started = true; + if (store.stopping) return; + if (!store.worker.joinable()) { + store.worker = std::thread(runJobs, std::ref(store)); + runningWorkers.fetch_add(1, std::memory_order_acq_rel); } - ++store.pending[device]; + auto& pending = store.pending[device]; store.jobs.emplace_back(device, std::move(job)); + ++pending; store.work.notify_one(); } @@ -108,6 +118,16 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip 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); + ++store.activeBuilds[context.device]; + struct ActiveBuild { + LibraryStore& store; + VkDevice device; + std::unique_lock& lock; + ~ActiveBuild() { + if (!lock.owns_lock()) lock.lock(); + if (--store.activeBuilds[device] == 0) store.idle.notify_all(); + } + } activeBuild{store, context.device, lock}; auto& device = store.devices[context.device]; if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); @@ -204,13 +224,16 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip break; } VkPipeline handle = VK_NULL_HANDLE; - if (partsOnly) lock.unlock(); + // Built without the store's mutex: a draw whose parts are resident is not held behind + // another thread's build, and builds from several threads (the registration prebuild, the + // journal replay) run at once; a part another thread built meanwhile is kept, this one dropped. + lock.unlock(); VkResult created; { auto pipelineCacheLock = context.LockPipelineCacheForUse(); created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); } - if (partsOnly) lock.lock(); + lock.lock(); Check(created, "vkCreateGraphicsPipelines library"); if (const auto raced = cache.find(key); raced != cache.end()) { device.destroyPipeline(context.device, handle, nullptr); @@ -238,10 +261,12 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; create.layout = info.layout; VkPipeline pipeline = VK_NULL_HANDLE; + lock.unlock(); { auto pipelineCacheLock = context.LockPipelineCacheForUse(); Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); } + lock.lock(); ++device.counters.linked; if (optimize && optimized != nullptr) { auto slot = std::make_shared(); @@ -252,7 +277,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip const auto cache = context.pipelineCache; const auto cacheSynchronization = context.pipelineCacheSynchronization; const auto libraryLayout = fullLayout; - enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { + std::function relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); @@ -272,7 +297,13 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } std::lock_guard lock(store.mutex); ++devicePointer->counters.optimized; - }); + }; + if (PipelineJournalReplaying()) { + slot->relink = std::move(relink); + slot->deferred.store(true, std::memory_order_release); + } else { + enqueue(store, context.device, std::move(relink)); + } } return pipeline; } @@ -282,6 +313,15 @@ void OptimizedPipeline::Release() noexcept { if (const auto pipeline = handle.exchange(VK_NULL_HANDLE)) destroy(device, pipeline, nullptr); } +void OptimizedPipeline::Schedule() { + if (!deferred.load(std::memory_order_acquire)) return; + auto& store = Libraries(); + std::lock_guard lock(store.mutex); + if (!deferred.exchange(false, std::memory_order_acq_rel)) return; + if (relink && !released.load()) enqueue(store, device, std::move(relink)); + relink = nullptr; +} + void WaitForOptimizedPipelines(VkDevice device) { auto& store = Libraries(); std::unique_lock lock(store.mutex); @@ -291,7 +331,7 @@ void WaitForOptimizedPipelines(VkDevice device) { void ClearPipelineLibraries(VkDevice device) { auto& store = Libraries(); std::unique_lock lock(store.mutex); - store.idle.wait(lock, [&] { return store.pending[device] == 0; }); + store.idle.wait(lock, [&] { return store.pending[device] == 0 && store.activeBuilds[device] == 0; }); const auto found = store.devices.find(device); if (found == store.devices.end()) return; auto& libraries = found->second; @@ -303,6 +343,27 @@ void ClearPipelineLibraries(VkDevice device) { store.devices.erase(found); } +void StopPipelineLibraryWorker() { + auto& store = Libraries(); + std::thread worker; + { + std::lock_guard lock(store.mutex); + store.stopping = true; + for (const auto& [device, job] : store.jobs) --store.pending[device]; + store.jobs.clear(); + worker = std::move(store.worker); + } + store.idle.notify_all(); + store.work.notify_all(); + if (!worker.joinable()) return; + if (worker.get_id() == std::this_thread::get_id()) worker.detach(); + else worker.join(); +} + +unsigned PipelineLibraryWorkers() { + return runningWorkers.load(std::memory_order_acquire); +} + PipelineLibraryCounters PipelineLibraryCountersOf(VkDevice device) { auto& store = Libraries(); std::lock_guard lock(store.mutex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index b494d51ffe..1f03eea443 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" @@ -3315,6 +3316,13 @@ void Recorder::Submit() { for (const auto& [begin, end] : notedReads(*open)) open->readGeneration = std::max(open->readGeneration, GuestMemory::CollectWrites(begin, end - begin)); } auto batch = std::move(open); + struct DiscardUnsubmitted { + Recorder& recorder; + std::unique_ptr& batch; + ~DiscardUnsubmitted() { + if (batch != nullptr && !batch->submitted) recorder.discard(std::move(batch)); + } + } discardUnsubmitted{*this, batch}; Check(function(endCommandBuffer, "vkEndCommandBuffer")(batch->commands), "vkEndCommandBuffer recorder"); VkSubmitInfo submission{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submission.commandBufferCount = 1; @@ -3331,7 +3339,12 @@ void Recorder::Submit() { submission.pSignalSemaphores = &timeline; } const auto submitStart = std::chrono::steady_clock::now(); - Check(function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence), "vkQueueSubmit recorder"); + const auto result = function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence); + if (result == VK_ERROR_DEVICE_LOST) { + const auto idle = context.Function("vkDeviceWaitIdle")(context.device); + if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate(); + } + Check(result, "vkQueueSubmit recorder"); batch->submitted = true; batch->serial = ++submissions; batch->submittedAt = std::chrono::steady_clock::now(); @@ -3828,7 +3841,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool try { action(); } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); + AgcDriver::LogCoalescedStderr_nid_no_patch("[gpu] deferred write-back failed: %s\n", error.what()); } } } @@ -3860,18 +3873,34 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool } // The batch's label entries leave the table (a later label to the same dword already replaced // its entry and belongs to another batch). Correctness never depended on this removal. - if (!batch->labelDwords.empty() || !batch->wideLabels.empty()) { + removeLabels(*batch); + release(*batch); +} + +void Recorder::discard(std::unique_ptr batch) noexcept { + removeLabels(*batch); + if (batch->completionLabelCount != 0) completionLabels.fetch_sub(batch->completionLabelCount, std::memory_order_acq_rel); + if (batch->writeBackCompletionCount != 0) writeBackCompletions.fetch_sub(batch->writeBackCompletionCount, std::memory_order_acq_rel); + if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); + release(*batch); + try { + publishPendingWrites(); + } catch (...) { + } +} + +void Recorder::removeLabels(Batch& batch) { + if (!batch.labelDwords.empty() || !batch.wideLabels.empty()) { std::lock_guard tableLock(labelTableMutex); - for (const auto dword : batch->labelDwords) { + for (const auto dword : batch.labelDwords) { const auto found = labels.find(dword); - if (found != labels.end() && found->second.batch == batch.get()) labels.erase(found); + if (found != labels.end() && found->second.batch == &batch) labels.erase(found); } - for (const auto entry : batch->wideLabels) wideLabels.erase(entry); + for (const auto entry : batch.wideLabels) wideLabels.erase(entry); recordedLabels.store(labels.size(), std::memory_order_relaxed); } - batch->labelDwords.clear(); - batch->wideLabels.clear(); - release(*batch); + batch.labelDwords.clear(); + batch.wideLabels.clear(); } void Recorder::release(Batch& batch) noexcept { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index a5ab4d1d84..8e5b642c4e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -151,7 +151,10 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa memory = allocation->memory; allocationBytes = allocation->allocationBytes; capacity = allocation->bytes; - if (allocation->bufferBytes == size) return; + if (allocation->bufferBytes == size) { + ready = true; + return; + } context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); buffer = VK_NULL_HANDLE; try { @@ -160,6 +163,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa release(); throw; } + ready = true; return; } try { @@ -177,6 +181,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa hostPlaced = allocation.memoryTypeIndex != context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device"); + ready = true; } catch (...) { release(); throw; @@ -188,7 +193,7 @@ DeviceBuffer::~DeviceBuffer() { } void DeviceBuffer::release() noexcept { - if (buffer && memory && cache && !hostPlaced) { + if (ready && buffer && memory && cache && !hostPlaced) { cache->Put({buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, false, size}); return; } @@ -373,9 +378,9 @@ void CommandBatch::Submit() { submission.pCommandBuffers = &commands; timing.Mark("command_end"); Check(context.Function("vkQueueSubmit")(context.queue, 1, &submission, fence), "vkQueueSubmit graphics"); - timing.Mark("queue_submit"); pending = true; submitted = true; + timing.Mark("queue_submit"); } void CommandBatch::Wait() { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index cade6d3a3d..2915b76a5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -1,10 +1,119 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include #include #include #include +#include +#include +#include +#include namespace AgcDriver::Graphics { +namespace { + +std::uint32_t pointerOperandStored(std::uint32_t opcode, std::span instruction) { + switch (static_cast(opcode)) { + case spv::OpStore: + case spv::OpCopyMemory: + case spv::OpCopyMemorySized: + case spv::OpAtomicStore: + case spv::OpAtomicFlagClear: + case spv::OpCooperativeMatrixStoreKHR: + case spv::OpCooperativeMatrixStoreNV: + case spv::OpCooperativeMatrixStoreTensorNV: + return instruction.size() > 1 ? instruction[1] : 0u; + case spv::OpAtomicExchange: + case spv::OpAtomicCompareExchange: + case spv::OpAtomicCompareExchangeWeak: + case spv::OpAtomicIIncrement: + case spv::OpAtomicIDecrement: + case spv::OpAtomicIAdd: + case spv::OpAtomicISub: + case spv::OpAtomicSMin: + case spv::OpAtomicUMin: + case spv::OpAtomicSMax: + case spv::OpAtomicUMax: + case spv::OpAtomicAnd: + case spv::OpAtomicOr: + case spv::OpAtomicXor: + case spv::OpAtomicFlagTestAndSet: + case spv::OpAtomicFMinEXT: + case spv::OpAtomicFMaxEXT: + case spv::OpAtomicFAddEXT: + return instruction.size() > 3 ? instruction[3] : 0u; + default: + return 0u; + } +} + +bool definesNoPointerValue(std::uint32_t opcode) { + switch (static_cast(opcode)) { + case spv::OpTypePointer: + case spv::OpTypeUntypedPointerKHR: + case spv::OpTypeForwardPointer: + case spv::OpName: + case spv::OpMemberName: + case spv::OpDecorate: + case spv::OpMemberDecorate: + case spv::OpDecorateId: + case spv::OpDecorateString: + case spv::OpMemberDecorateString: + return true; + default: + return false; + } +} + +template +bool forEachInstruction(std::span words, const TVisit& visit) { + for (std::size_t at = 5; at < words.size();) { + const auto count = words[at] >> 16u; + if (count == 0 || at + count > words.size()) return false; + visit(words[at] & 0xffffu, words.subspan(at, count)); + at += count; + } + return true; +} + +bool storesThroughAddressesCached(const ShaderRecompiler::SharedSpirv& spirv) { + static std::mutex mutex; + static std::map, bool> known; + const auto hash = spirv.ContentHash(); + const std::pair key{hash.low, hash.high}; + { + std::lock_guard lock(mutex); + if (const auto found = known.find(key); found != known.end()) return found->second; + } + const bool stores = SpirvStoresThroughAddresses(spirv.Words()); + std::lock_guard lock(mutex); + known.emplace(key, stores); + return stores; +} + +} + +bool SpirvStoresThroughAddresses(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) return true; + std::unordered_set pointerTypes; + const bool readable = forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if ((opcode == spv::OpTypePointer || opcode == spv::OpTypeUntypedPointerKHR) && instruction.size() > 2 && instruction[2] == spv::StorageClassPhysicalStorageBuffer) pointerTypes.insert(instruction[1]); + }); + if (!readable) return true; + if (pointerTypes.empty()) return false; + std::unordered_set pointers; + forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if (instruction.size() > 2 && !definesNoPointerValue(opcode) && pointerTypes.contains(instruction[1])) pointers.insert(instruction[2]); + }); + bool stores = false; + forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + const auto pointer = pointerOperandStored(opcode, instruction); + const bool extendedWithPointer = opcode == spv::OpExtInst && std::any_of(instruction.begin() + static_cast(std::min(5u, instruction.size())), instruction.end(), [&](std::uint32_t operand) { return pointers.contains(operand); }); + stores = stores || extendedWithPointer || (pointer != 0 && pointers.contains(pointer)); + }); + return stores; +} + void ShaderResources::prepareAddressBindings(std::span shaders, std::span snapshots) { static const bool loopGuarded = std::getenv("APS5_LOOP_GUARD") != nullptr; for (const auto& shader : shaders) { @@ -22,6 +131,7 @@ void ShaderResources::prepareAddressBindings(std::span sha Require(shader.program->bdaAbiVersion == (faults == 0 ? 0u : ShaderRecompiler::BdaAbi::Version), "incompatible BDA ABI version"); // Rect-list validation needs a fault buffer, but never accesses guest addresses. usesBda = usesBda || tables != 0; + if (tables != 0 && storesThroughAddressesCached(shader.program->spirv)) bdaReadOnly = false; usesFaultBuffer = usesFaultBuffer || faults != 0; const bool tableFaultsOnly = std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTableMap; }); faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || loopGuarded)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 15a0f3c2d2..01adf45e3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -145,6 +146,7 @@ struct TextureCache { std::chrono::steady_clock::time_point budgetRead{}; std::uint64_t budget = 0; std::uint64_t reportedBudget = 0; + std::uint64_t reportedStorage = 0; std::chrono::steady_clock::time_point reportedAt{}; std::uint64_t erasures = 0; }; @@ -154,6 +156,26 @@ TextureCache& Textures() { return cache; } +thread_local unsigned sampledCacheHeld = 0; +thread_local unsigned storageCacheHeld = 0; + +struct CacheHold { + unsigned& depth; + explicit CacheHold(unsigned& depth) : depth(depth) { ++depth; } + ~CacheHold() { --depth; } + CacheHold(const CacheHold&) = delete; + CacheHold& operator=(const CacheHold&) = delete; +}; + +bool AcquireCache(std::mutex& mutex) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20); + do { + if (mutex.try_lock()) return true; + std::this_thread::yield(); + } while (std::chrono::steady_clock::now() < deadline); + return false; +} + struct UncachedImages { std::mutex mutex; std::vector> images; @@ -516,7 +538,7 @@ std::uint64_t storageBudget(const Context& context) { context.memoryProperties2(context.physical, &properties); cached.device = context.physical; cached.read = now; - cached.budget = StorageTextureBudget(context.memory, &reported); + cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); return cached.budget; } @@ -533,11 +555,17 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage); const bool fresh = cache.budgetDevice != context.physical; - if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { + const auto storageLimit = storageBudget(context); + if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt) || SampledBudgetReportDue(cache.reportedStorage, storageLimit, now - cache.reportedAt))) { char was[48] = ""; - if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); - std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageBudget(context) >> 20u)); + char wasStorage[48] = ""; + if (!fresh) { + std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); + std::snprintf(wasStorage, sizeof(wasStorage), " (was %llu MiB)", static_cast(cache.reportedStorage >> 20u)); + } + std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB%s\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageLimit >> 20u), wasStorage); cache.reportedBudget = budget; + cache.reportedStorage = storageLimit; cache.reportedAt = now; } cache.budgetDevice = context.physical; @@ -713,6 +741,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spansource != nullptr) { @@ -953,6 +982,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std std::shared_ptr texture; { std::lock_guard lock(cache.mutex); + const CacheHold held(storageCacheHeld); if (resource.mipCount > AllocatedLevels(resource)) { auto allocated = resource; allocated.mipCount = AllocatedLevels(resource); @@ -995,6 +1025,11 @@ std::shared_ptr cachedStorageTexture(const Context& context, std for (auto& resource : resources) recorder->Keep(std::move(resource)); } } + if (!evicted.empty()) { + auto& sampled = Textures(); + std::lock_guard lock(sampled.mutex); + dropUncachedViews(sampled); + } return texture; } @@ -1005,10 +1040,14 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } -std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes) { const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); - return std::max(512ull << 20u, std::min(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u)); + const auto budget = reported->heapBudget[*heap]; + const auto usage = reported->heapUsage[*heap]; + const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + std::max(2048ull << 20u, sampledBytes) + budget / 8u; + const auto cap = std::min(TextureCacheBudget(memory), budget / 4u); + return std::max(512ull << 20u, std::min(cap, budget > reserved ? budget - reserved : 0u)); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { @@ -1025,6 +1064,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c return change > reported / 10u && sinceReport >= std::chrono::seconds(10); } +std::uint64_t StorageTextureCacheBudget(const Context& context) { + return storageBudget(context); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { @@ -1104,6 +1147,90 @@ void ClearCachedTextures(VkDevice device) { } } +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { + thread_local bool releasing = false; + if (releasing) return 0; + releasing = true; + struct Done { + bool& flag; + ~Done() { flag = false; } + } done{releasing}; + std::uint64_t freed = 0; + std::size_t sampledEntries = 0; + std::size_t storageEntries = 0; + std::size_t storageKept = 0; + bool sampledSkipped = false; + bool storageSkipped = false; + { + auto& cache = Textures(); + if (sampledCacheHeld != 0 || !AcquireCache(cache.mutex)) { + sampledSkipped = true; + } else { + std::lock_guard lock(cache.mutex, std::adopt_lock); + dropUncachedViews(cache); + while (freed < bytes && !cache.entries.empty()) { + const auto back = std::prev(cache.entries.end()); + const auto accounted = back->accounted; + const bool last = back->texture.use_count() == 1; + eraseTexture(cache, back); + ++cache.counts.evicted; + ++sampledEntries; + if (last) freed += accounted; + } + } + } + if (freed < bytes) { + auto& cache = StorageTextures(); + std::unique_lock gpu(GuestMemory::GpuMutex(), std::defer_lock); + if (storageCacheHeld != 0 || !gpu.try_lock()) { + storageSkipped = true; + } else { + try { + while (freed < bytes) { + std::vector, std::uint64_t>> evicted; + { + if (!AcquireCache(cache.mutex)) { + storageSkipped = true; + break; + } + std::lock_guard lock(cache.mutex, std::adopt_lock); + std::uint64_t batch = 0; + storageKept = 0; + for (auto it = cache.entries.end(); it != cache.entries.begin() && batch + freed < bytes;) { + --it; + if (it->texture->Dirty()) { + ++storageKept; + continue; + } + const auto accounted = it->accounted; + const auto next = std::next(it); + evicted.emplace_back(evictStorage(cache, it), accounted); + batch += accounted; + it = next; + } + if (evicted.empty()) break; + } + for (auto& [image, accounted] : evicted) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); + } + resources.clear(); + ++storageEntries; + if (image.use_count() == 1) freed += accounted; + image.reset(); + } + } + } catch (const std::exception& error) { + std::fprintf(stderr, "[gpu] texture cache release stopped: %s\n", error.what()); + } + } + } + std::fprintf(stderr, "[gpu] texture caches released %llu MiB of the %llu MiB asked for: %zu sampled entries%s, %zu storage images%s, %zu with pending results kept\n", static_cast(freed >> 20u), static_cast(bytes >> 20u), sampledEntries, sampledSkipped ? " (cache busy, skipped)" : "", storageEntries, storageSkipped ? " (cache or device busy, skipped)" : "", storageKept); + return freed; +} + bool StorageImageCached(const Context& context, const StorageTexture* image) { auto& cache = StorageTextures(); std::lock_guard lock(cache.mutex); @@ -1155,6 +1282,7 @@ const char* roleName(ShaderRecompiler::DescriptorRole role) { case ShaderRecompiler::DescriptorRole::ImageTable: return "ImageTable"; case ShaderRecompiler::DescriptorRole::SamplerTable: return "SamplerTable"; case ShaderRecompiler::DescriptorRole::ImageTableMap: return "ImageTableMap"; + case ShaderRecompiler::DescriptorRole::ImageStorageTable: return "ImageStorageTable"; } throw std::runtime_error("AGC graphics: unknown descriptor role"); } @@ -1492,6 +1620,10 @@ std::string HexText(std::uint64_t value) { return text; } +std::string DescriptorLimitError(std::string_view description, std::uint64_t required, std::uint64_t limit) { + return std::string(description) + ": required " + std::to_string(required) + ", device limit " + std::to_string(limit); +} + void ValidateDescriptorLayout(const Context& context, std::span bindings, bool updateAfterBind) { const auto maxStageResources = updateAfterBind ? context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; std::map heapTotals; @@ -1507,10 +1639,11 @@ void ValidateDescriptorLayout(const Context& context, std::span context.limits.maxPerStageDescriptorSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed per-stage sampler capacity", count, context.limits.maxPerStageDescriptorSamplers)); } } - Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= context.limits.maxDescriptorSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages && heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] <= context.limits.maxDescriptorSetSamplers, "typed heaps exceed descriptor set capacity"); + Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= context.limits.maxDescriptorSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages, "typed heaps exceed descriptor set capacity"); + if (heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] > context.limits.maxDescriptorSetSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed descriptor set sampler capacity", heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER], context.limits.maxDescriptorSetSamplers)); } void ValidateRuntimeResources(const CompiledShader& shader, std::span words) { @@ -1575,10 +1708,13 @@ void ShaderResources::buildPrepare(std::span shaders, cons }; for (const auto& binding : shader.program->bindings) { Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set"); - if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) { - Require(binding.binding == ShaderRecompiler::RuntimeAbi::ImageTableBinding && binding.kind == ShaderRecompiler::DescriptorKind::SampledImage && binding.guestDescriptor.size() == static_cast(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count, "image table binding is malformed"); + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) { + const bool storage = binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable; + Require(binding.binding == (storage ? ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding : ShaderRecompiler::RuntimeAbi::ImageTableBinding) && binding.kind == (storage ? ShaderRecompiler::DescriptorKind::StorageImage : ShaderRecompiler::DescriptorKind::SampledImage) && binding.guestDescriptor.size() == static_cast(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count, "image table binding is malformed"); auto& table = tableStage(shader); - Require(table.images == nullptr, "a stage binds two image tables"); + Require(storage ? !table.storage : !table.sampled, "a stage binds two image tables"); + Require(table.images == nullptr || table.images->count == binding.count, "a stage's image tables disagree on their elements"); + (storage ? table.storage : table.sampled) = true; table.images = &binding; table.firstSampler = firstSampler; continue; @@ -1673,6 +1809,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons } splitSets = SplitsSets(context, shaders); std::array tableFlags{}; + std::array storageTableFlags{}; for (auto& table : tableStages) { Require(table.mapAllocation != std::numeric_limits::max(), "an image table stage has no map"); const auto& map = table.map; @@ -1687,7 +1824,13 @@ void ShaderResources::buildPrepare(std::span shaders, cons table.elementBase = tableElements[table.set]; table.map[ShaderRecompiler::ImageTableAbi::HeaderElementBase] = table.elementBase; tableElements[table.set] += elements; - tableFlags[table.set] |= VulkanStage(table.stage); + if (table.sampled) tableFlags[table.set] |= VulkanStage(table.stage); + if (table.storage) storageTableFlags[table.set] |= VulkanStage(table.stage); + } + for (std::uint32_t set = 0; set < storageTableFlags.size(); ++set) { + if (storageTableFlags[set] == 0) continue; + bindings.push_back({{ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, StorageTableCapacity(context), storageTableFlags[set], nullptr}, {}, {}, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, static_cast(set)}); + storageTableBindings[set] = bindings.size() - 1u; } for (std::uint32_t set = 0; set < tableFlags.size(); ++set) { if (tableFlags[set] == 0) continue; @@ -1788,7 +1931,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (storageBuffers != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(storageBuffers)}); const auto tableDescriptors = tableBindings[0] != std::numeric_limits::max() ? tableDescriptorCount(0) : 0u; if (plannedSampledImages + tableDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, plannedSampledImages + tableDescriptors}); - if (plannedStorageImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages}); + const auto storageTableDescriptors = storageTableBindings[0] != std::numeric_limits::max() ? bindings[storageTableBindings[0]].layout.descriptorCount : 0u; + if (plannedStorageImages + storageTableDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages + storageTableDescriptors}); std::uint32_t samplerDescriptors = 0; for (const auto& binding : bindings) { if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) samplerDescriptors += binding.layout.descriptorCount; @@ -1838,6 +1982,7 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span, 2> bindingFlags; std::array tableFlags{}; + std::array storageTableFlags{}; bool updateAfterBind = false; std::uint64_t storageBuffers = 0; for (const auto& shader : shaders) { @@ -1851,6 +1996,10 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::spanHandle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } writes.push_back(write); @@ -1989,6 +2144,7 @@ void ShaderResources::buildComplete() { write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); + else if (storageTextures[index] == nullptr) images.push_back({VK_NULL_HANDLE, multisampleStorageViews[index], VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); @@ -2061,6 +2217,7 @@ void mixDataWords(std::uint64_t& hash, std::span words) { // ResourceCache::Find and Touch calls (the [rescache] line). std::atomic resourceCacheFinds{0}; std::atomic resourceCacheTouches{0}; +std::atomic depthGuardRefusals{0}; } @@ -2103,6 +2260,10 @@ void ShaderResources::noteReusable() { reusable = true; } +std::uint64_t ShaderResources::DepthGuardRefusals() { + return depthGuardRefusals.load(std::memory_order_relaxed); +} + bool ShaderResources::NeverReusable(std::span shaders) { const bool loopGuard = LoopGuardEnabled(); return std::any_of(shaders.begin(), shaders.end(), [loopGuard](const CompiledShader& shader) { @@ -2573,6 +2734,10 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanDescriptor(); const auto address = own.baseAddress; const auto bytes = static_cast(image->GuestBytes()); + if (DepthSurfaceOverlaps(context.device, address, bytes)) { + depthGuardRefusals.fetch_add(1, std::memory_order_relaxed); + return fail(FastFail::Depth); + } if (GuestMemory::CollectWrites(address, bytes) == 0) return fail(FastFail::Collect); if (own.dccAddress != 0) { // Refresh's unchanged branch (its key compare against the keys the content was @@ -2676,6 +2841,10 @@ bool ShaderResources::fastRevalidateEach() { if (own.dccAddress != 0) return false; const auto address = own.baseAddress; const auto bytes = static_cast(image->GuestBytes()); + if (DepthSurfaceOverlaps(context.device, address, bytes)) { + depthGuardRefusals.fetch_add(1, std::memory_order_relaxed); + return false; + } if (GuestMemory::CollectWrites(address, bytes) == 0) return false; if (PendingStorageOverlaps(address, bytes, image)) return false; if (!StorageImageCached(context, image)) return false; @@ -3372,7 +3541,7 @@ ResourceCache& SharedResourceCache() { return *cache; } -DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), resetPool(context.Function("vkResetDescriptorPool")) {} +DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), resetPool(context.Function("vkResetDescriptorPool")), freeDescriptorSets(context.individuallyFreeDescriptorSets ? context.Function("vkFreeDescriptorSets") : nullptr) {} DescriptorCache::~DescriptorCache() { for (const auto& pool : pools) destroyPool(context.device, pool.handle, nullptr); @@ -3399,7 +3568,12 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span key ValidateDescriptorLayout(context, bindings, updateAfterBind); ++stats.layoutMisses; const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags); - layouts.emplace(std::move(keyCopy), layout); + try { + layouts.emplace(std::move(keyCopy), layout); + } catch (...) { + destroyLayout(context.device, layout, nullptr); + throw; + } return layout; } @@ -3470,9 +3644,11 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l } VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; poolInfo.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; + if (context.individuallyFreeDescriptorSets) poolInfo.flags |= VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; poolInfo.maxSets = ChainPoolSets; poolInfo.poolSizeCount = static_cast(ChainPoolCreateSizes.size()); poolInfo.pPoolSizes = ChainPoolCreateSizes.data(); + chain.reserve(chain.size() + 1); VkDescriptorPool pool = VK_NULL_HANDLE; Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool"); chain.push_back({pool}); @@ -3525,6 +3701,7 @@ void DescriptorCache::Free(const SetAllocation& allocation) noexcept { if (allocation.poolIndex >= chain.size()) std::terminate(); auto& pool = chain[allocation.poolIndex]; if (pool.handle != allocation.pool || pool.liveSets == 0) std::terminate(); + if (freeDescriptorSets != nullptr && freeDescriptorSets(context.device, allocation.pool, 1, &allocation.set) != VK_SUCCESS) std::terminate(); if (--pool.liveSets != 0) return; if (resetPool(context.device, pool.handle, 0) != VK_SUCCESS) std::terminate(); pool.exhausted = false; @@ -3535,29 +3712,42 @@ DescriptorCache::Stats DescriptorCache::Counters() const { return stats; } +std::uint64_t ShaderResources::guestBufferBytes(std::span words) const { + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + const auto address = descriptor.Base48(); + auto byteSize = descriptor.GetSize(); + if (byteSize > context.limits.maxStorageBufferRange) { + const auto mappedEnd = RegisteredReadableEnd(address); + if (mappedEnd <= address) byteSize = 0; + else if (mappedEnd - address < context.limits.maxStorageBufferRange) byteSize = mappedEnd - address; + } + if (byteSize == 0 || address == 0) return 0; + if (const std::uint64_t stride = descriptor.Stride(); stride != 0 && byteSize <= context.limits.maxStorageBufferRange) { + constexpr std::uint64_t GuestPageBytes = 0x4000; + const auto end = address + byteSize; + const auto overhang = end % GuestPageBytes; + if (overhang != 0 && overhang < stride) { + const auto allocationEnd = RegisteredReadableEnd(address); + if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) byteSize = allocationEnd - address; + } + } + return byteSize; +} + std::size_t ShaderResources::addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) { Require(words.size() == 4, "buffer descriptor must contain four DWORDs"); Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set"); const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); - const auto byteSize = descriptor.GetSize(); + const auto byteSize = guestBufferBytes(words); if (byteSize == 0 || address == 0) { allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); - auto size = static_cast(byteSize); - if (const std::uint64_t stride = descriptor.Stride(); stride != 0) { - constexpr std::uint64_t GuestPageBytes = 0x4000; - const auto end = address + size; - const auto overhang = end % GuestPageBytes; - if (overhang != 0 && overhang < stride) { - const auto allocationEnd = RegisteredReadableEnd(address); - if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) size = static_cast(allocationEnd - address); - } - } + const auto size = static_cast(byteSize); Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; @@ -3776,7 +3966,7 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& Require(plannedSampledImages <= context.limits.maxDescriptorSetSampledImages, "pipeline sampled-image descriptors exceed device limits"); } else { Require(elementWords == 4, "guest sampler descriptor must contain 4 dwords"); - Require(capacity <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits"); + if (capacity > context.limits.maxPerStageDescriptorSamplers) throw std::runtime_error(DescriptorLimitError("shader sampler descriptors exceed per-stage limits", capacity, context.limits.maxPerStageDescriptorSamplers)); Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); for (std::uint32_t element = 0; element < binding.count; ++element) { @@ -4046,6 +4236,21 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; if (binding.imageShape.has_value() && !firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest storage 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)) + ")"); + if (resource.dimension == TextureDimension::k2DMsaa) { + Require(!(element < binding.imageAtomic.size() && binding.imageAtomic[element]) && !(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]), "storage image atomics on a multisampled texture are not implemented"); + storageTextures.push_back(nullptr); + multisampleStorageViews.resize(storageTextures.size(), VK_NULL_HANDLE); + multisampleStorageViews.back() = MultisampleTargetStorageView(context, resource); + multisampleSources.push_back(resource.baseAddress); + storageMips.push_back(0); + storageKeys.push_back(0); + storageFirstLayer.push_back(false); + storageWritten.push_back(false); + storageAtomic.push_back(false); + storageAtomic64.push_back(false); + item.imageAllocations.push_back(storageTextures.size() - 1); + continue; + } 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; @@ -4067,6 +4272,10 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } } +std::uint32_t StorageTableCapacity(const Context& context) { + return std::min({context.imageTableCapacity, 4096u, context.limits.maxPerStageDescriptorStorageImages / 4u}); +} + std::uint32_t ImageTableCapacity(const Context& context) { if (!context.runtimeDescriptorArray || !context.partiallyBound || !context.variableDescriptorCount || !context.descriptorIndexing) return 0u; constexpr std::uint64_t sampledHeaps = static_cast(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) * ShaderRecompiler::RuntimeAbi::SampledHeapCapacity; @@ -4162,14 +4371,16 @@ void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding& } void ShaderResources::resolveImageTables() { - if (tableBindings[0] == std::numeric_limits::max() && tableBindings[1] == std::numeric_limits::max()) return; + const auto none = [](const std::array& set) { return set[0] == std::numeric_limits::max() && set[1] == std::numeric_limits::max(); }; + if (none(tableBindings) && none(storageTableBindings)) return; auto& counters = TextureCounts(); for (std::size_t set = 0; set < tableBindings.size(); ++set) { + if (storageTableBindings[set] != std::numeric_limits::max()) bindings[storageTableBindings[set]].imageAllocations.assign(std::min(tableElements[set], StorageTableCapacity(context)), std::numeric_limits::max()); if (tableBindings[set] != std::numeric_limits::max()) bindings[tableBindings[set]].imageAllocations.assign(std::min(tableElements[set], context.imageTableCapacity), std::numeric_limits::max()); } for (auto& table : tableStages) { if (table.images == nullptr) continue; - auto& item = bindings[tableBindings[table.set]]; + auto* item = tableBindings[table.set] != std::numeric_limits::max() ? &bindings[tableBindings[table.set]] : nullptr; const auto& binding = *table.images; const auto faults = table.map[ShaderRecompiler::ImageTableAbi::HeaderElementFaults]; const auto stageSamplers = std::span>(samplers).subspan(table.firstSampler, table.samplerCount); @@ -4198,11 +4409,12 @@ void ShaderResources::resolveImageTables() { textureFirstLayer.push_back(firstLayer); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); table.elementTextures[element] = textures.size() - 1u; - item.imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; + item->imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; }; const auto state = CurrentTableState(); std::vector> cached; - if (auto* reused = FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) { + const bool storageTable = std::find(binding.imageWritten.begin(), binding.imageWritten.end(), true) != binding.imageWritten.end(); + if (auto* reused = storageTable ? nullptr : FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) { auto next = cached.begin(); for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); @@ -4246,6 +4458,45 @@ void ShaderResources::resolveImageTables() { if (reusable) fresh.elements.push_back({{}, {}, {}, 0, false, true, error.what()}); continue; } + if (element < binding.imageWritten.size() && binding.imageWritten[element]) { + reusable = false; + const auto storageBinding = storageTableBindings[table.set]; + if (storageBinding == std::numeric_limits::max()) { + table.map[faults + element] = rejectTableElement(table, words, "set " + std::to_string(table.set) + " has no image storage table, but the shader stores to its table element " + std::to_string(absolute)); + continue; + } + if (absolute >= StorageTableCapacity(context)) { + table.map[faults + element] = rejectTableElement(table, words, "the stored table element " + std::to_string(absolute) + " of set " + std::to_string(table.set) + " is past the image storage table's capacity of " + std::to_string(StorageTableCapacity(context)) + " images"); + continue; + } + CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); + if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); + const auto mip = std::min(resource.baseLevel, resource.mipCount - 1u); + if (resource.minLod > mip * 256u) { + table.map[faults + element] = rejectTableElement(table, words, "the table entry's storage T# clamps its minimum LOD above the level it addresses, which is not implemented"); + continue; + } + try { + storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes, true)); + } catch (const std::runtime_error& error) { + table.map[faults + element] = rejectTableElement(table, words, error.what()); + continue; + } + storageMips.push_back(mip); + storageKeys.push_back(resource.dccAddress); + storageFirstLayer.push_back(firstLayer); + storageWritten.push_back(true); + storageAtomic.push_back(false); + storageAtomic64.push_back(false); + describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); + bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; + continue; + } + if (item == nullptr) { + table.map[faults + element] = rejectTableElement(table, words, "a sampled image table entry has no sampled image table binding"); + reusable = false; + continue; + } CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; @@ -4332,15 +4583,20 @@ void ShaderResources::finishImageTables() { bda->SetImageTables(std::move(faults), std::move(ranges)); } -std::vector> ShaderResources::PresyncSurfaces() const { - std::vector> surfaces; +std::vector ShaderResources::PresyncSurfaces() const { + std::vector surfaces; // A surface's lookup reads guest memory on the CPU unless it is served GPU-direct from a host // import: a storage image of imported memory uploads and refreshes from the import, and a // sampled texture of one views that image (SampledFromStorageEligible), so neither waits for // the producer. const auto consider = [&](std::uint32_t format, std::uint64_t address, std::uint64_t guestBytes, bool sampled) { const bool gpuDirect = sampled ? SampledFromStorageEligible(context, format, address, guestBytes) : HostImportCovers(context, address, static_cast(guestBytes)); - if (!gpuDirect) surfaces.emplace_back(address, guestBytes); + if (!gpuDirect) surfaces.push_back({address, guestBytes, false}); + }; + static const bool keyRanges = std::getenv("APS5_NO_DCC_PRESYNC") == nullptr; + const auto considerKeys = [&](std::uint64_t dccAddress, std::uint64_t guestBytes) { + const auto keyBytes = DccKeyBytes(guestBytes); + if (keyRanges && dccAddress != 0 && keyBytes != 0) surfaces.push_back({dccAddress, keyBytes, true}); }; if (completed) { // A cached object (the resource cache): Revalidate repeats the lookups of the objects the @@ -4356,18 +4612,22 @@ std::vector> ShaderResources::PresyncSur const auto& texture = textures[textureIndex++]; if (texture->ViewsStorageImage()) consider(range.format, range.address, range.bytes, false); else consider(range.format, range.address, range.bytes, true); + considerKeys(range.dccAddress, range.bytes); } } for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (index != 0 && storageTextures[index] == storageTextures[index - 1]) continue; + if (storageTextures[index] == nullptr || (index != 0 && storageTextures[index] == storageTextures[index - 1])) continue; const auto& image = *storageTextures[index]; consider(image.Descriptor().format, image.Descriptor().baseAddress, image.GuestBytes(), false); + considerKeys(image.Descriptor().dccAddress, image.GuestBytes()); } return surfaces; } if (!imageRecords.empty()) { for (const auto& record : imageRecords) { - if (record.decoded) consider(record.resource.format, record.resource.baseAddress, record.guestBytes, record.sampled); + if (!record.decoded) continue; + consider(record.resource.format, record.resource.baseAddress, record.guestBytes, record.sampled); + considerKeys(record.resource.dccAddress, record.guestBytes); } return surfaces; } @@ -4376,14 +4636,18 @@ std::vector> ShaderResources::PresyncSur forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span words) { if (IsNullTextureResource(words)) return; const auto resource = DecodeTextureResource(words); - consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); + const auto guestBytes = DescribeSurface(resource).guestBytes; + consider(resource.format, resource.baseAddress, guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); + considerKeys(resource.dccAddress, guestBytes); }); for (const auto& binding : deferredCompute.program->bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue; for (std::size_t element = 0; element < binding.guestDescriptor.size() / 8u; ++element) { try { const auto resource = DecodeTextureResource(std::span(binding.guestDescriptor).subspan(element * 8u, 8u)); - consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, true); + const auto guestBytes = DescribeSurface(resource).guestBytes; + consider(resource.format, resource.baseAddress, guestBytes, true); + considerKeys(resource.dccAddress, guestBytes); } catch (const std::runtime_error&) { } } @@ -4489,13 +4753,71 @@ std::optional> ShaderResources::MovedR return moved; } +const char* ShaderResources::MoveRefusalName(MoveRefusal refusal) { + switch (refusal) { + case MoveRefusal::None: return "none"; + case MoveRefusal::Shape: return "bindings differ"; + case MoveRefusal::NotGuest: return "not read in place"; + case MoveRefusal::Written: return "written or emptied"; + case MoveRefusal::Limit: return "over the limit"; + case MoveRefusal::Inaccessible: return "unmapped"; + case MoveRefusal::Overlap: return "pending write"; + case MoveRefusal::Bindings: return "no bindings made"; + case MoveRefusal::Count: break; + } + return "?"; +} + +std::optional> ShaderResources::MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const { + proof = {}; + const auto refuse = [&](MoveRefusal why) { + proof.refusal = why; + return std::optional>{}; + }; + std::vector moved; + if (shader.program == nullptr || usesBda) return refuse(MoveRefusal::Shape); + if (_set == VK_NULL_HANDLE) return moved; + for (const auto& binding : shader.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + const auto kept = std::find_if(bindings.begin(), bindings.end(), [&](const Binding& item) { return item.layout.binding == binding.binding; }); + if (kept == bindings.end() || kept->allocations.size() != binding.count || binding.guestDescriptor.size() != static_cast(binding.count) * 4u) return refuse(MoveRefusal::Shape); + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + const auto address = descriptor.Base48(); + const auto size = guestBufferBytes(std::span(words, 4)); + const auto& item = allocations[kept->allocations[element]]; + const bool empty = size == 0 || address == 0; + if (!item.guest) { + if (empty) continue; + return refuse(MoveRefusal::NotGuest); + } + if (item.address == address && item.size == size) continue; + const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + if (written || empty || item.written) return refuse(MoveRefusal::Written); + if (size > context.limits.maxStorageBufferRange) return refuse(MoveRefusal::Limit); + const auto begin = address - item.adjustment; + const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); + if (maxBytes != 0 && proof.bytes + bytes > maxBytes) return refuse(MoveRefusal::Limit); + if (!GuestMemory::Accessible(reinterpret_cast(begin), bytes)) return refuse(MoveRefusal::Inaccessible); + const bool pending = recorder != nullptr ? recorder->PendingWriteOverlaps(begin, bytes) : Recorder::SnapshotWriteOverlaps(begin, bytes); + const std::array, 1> range{{{begin, begin + bytes}}}; + if (guestMemory.WritesOverlap(begin, bytes) || pending || StorageTexture::AnyPendingOverlaps(range) || AnyShadowedOverlaps(begin, bytes)) return refuse(MoveRefusal::Overlap); + moved.push_back({kept->allocations[element], address, static_cast(size)}); + ++proof.buffers; + proof.bytes += bytes; + } + } + return moved; +} + std::size_t ShaderResources::MaxDrawSnapshotBytes() { const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull; return static_cast(mib * 1024ull * 1024ull); } -std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { +std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved, bool movedOnly) const { if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = maxDrawSnapshotBytes; @@ -4535,7 +4857,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi address = override->address; size = override->size; } else { - if (!item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; + if (movedOnly || !item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return item.address >= range.first && item.address < range.second && item.size <= range.second - item.address; }); if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue; } @@ -4644,7 +4966,7 @@ void ShaderResources::WriteBack() { WriteBackBuffers(); if (SkipWriteBack()) return; for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index]) continue; + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; storageTextures[index]->MarkDirty(); static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } @@ -4663,7 +4985,7 @@ void ShaderResources::MarkGpuWrites(Recorder& recorder) { } if (SkipWriteBack()) return; for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index]) continue; + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; storageTextures[index]->MarkDirty(); static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 9c7dcb033e..f04ec34921 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -47,7 +47,7 @@ struct Module { bool position = false; std::uint32_t clipDistances = 0; std::uint32_t cullDistances = 0; - bool primitiveIndices = false; + std::uint32_t primitiveIndices = 0; bool fragmentBarycentric = false; bool layerOutput = false; std::map> modes; @@ -134,8 +134,9 @@ struct Module { Require(input && ((value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in"); 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") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in"); + if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u; + if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = 3u; + 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::BuiltInPrimitiveLineIndicesEXT && signature == "u32x2") || (!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); @@ -151,7 +152,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -185,6 +186,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi std::uint32_t entryPoint = 0; std::set executionModeTargets; bool upperLeft = false; + bool terminateExtension = false; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); @@ -331,9 +333,19 @@ 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; } + if (extension == "SPV_KHR_terminate_invocation") { + Require(fragment && shaderTerminateInvocation, "SPV_KHR_terminate_invocation requires enabled shaderTerminateInvocation in a fragment shader"); + terminateExtension = true; + break; + } Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } + case spv::OpTerminateInvocation: + Require(count == 1, "invalid OpTerminateInvocation instruction"); + Require(fragment && shaderTerminateInvocation, "OpTerminateInvocation requires enabled shaderTerminateInvocation in a fragment shader"); + Require(terminateExtension, "OpTerminateInvocation requires SPV_KHR_terminate_invocation"); + break; case spv::OpDecorateId: case spv::OpDecorationGroup: case spv::OpGroupDecorate: @@ -437,7 +449,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeLocalSize, {paired ? config.threadsPerGroup / 2u : config.threadsPerGroup, 1, 1}); mode(spv::ExecutionModeOutputVertices, {config.maxVertices}); mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives}); - mode(spv::ExecutionModeOutputTrianglesEXT, {}); + mode(config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {}); Require(module.modes.size() == 4, "unsupported mesh execution mode"); } else if (control) { Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); @@ -486,7 +498,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto length = module.constants.find(outer[3]); Require(length != module.constants.end() && length->second != 0, "invalid per-vertex interface array length"); if (mesh) { - const bool primitive = decoration.perPrimitive || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitiveTriangleIndicesEXT || *decoration.builtin == spv::BuiltInCullPrimitiveEXT)); + const bool primitive = decoration.perPrimitive || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitiveLineIndicesEXT || *decoration.builtin == spv::BuiltInPrimitiveTriangleIndicesEXT || *decoration.builtin == spv::BuiltInCullPrimitiveEXT)); Require(length->second == (primitive ? state.stages.mesh->maxPrimitives : state.stages.mesh->maxVertices), "mesh interface array disagrees with output limits"); } else if (geometry) { Require(length->second == 3u, "geometry interface array must hold the three vertices of a triangle"); @@ -537,10 +549,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(decoration.set && decoration.binding, "unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.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; }); - Require(descriptors.insert(key).second || (binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::ImageTable), "duplicate SPIR-V resource binding"); + Require(descriptors.insert(key).second || (binding != shader.bindings.end() && (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::ImageStorageTable)), "duplicate SPIR-V resource binding"); Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata"); Require(binding->kind == ShaderRecompiler::DescriptorKind::Sampler || binding->kind == ShaderRecompiler::DescriptorKind::SampledImage || binding->kind == ShaderRecompiler::DescriptorKind::StorageImage, "SPIR-V descriptor type disagrees with recompiler binding metadata"); - if (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::SamplerTable) { + if (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::ImageStorageTable || binding->role == ShaderRecompiler::DescriptorRole::SamplerTable) { Require(type.size() == 3 && (type[0] & 0xffffu) == spv::OpTypeRuntimeArray, "image table must be declared as a runtime descriptor array"); continue; } @@ -594,7 +606,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); 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 (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); return module; } @@ -602,7 +614,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) { - const auto& words = program.spirv.Words(); + const auto words = program.spirv.Words(); Require(words.size() >= 5 && words[0] == spv::MagicNumber, "the fragment shader is not a SPIR-V module"); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -615,7 +627,7 @@ bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program return false; } -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, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { +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, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -636,7 +648,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, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation); 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 c16eb7847a..abe15d5929 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -256,7 +256,7 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint } void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { - zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); + zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); @@ -384,6 +384,29 @@ constexpr std::array, 2> StandardLocations2{{{4, 4}, constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; +std::string nonTriangleRejection(const QueueState& queue, const char* message, bool rectangles = false) { + const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional { + const auto it = find(registers, offset, bank); + if (it == registers.end()) return std::nullopt; + return it->second; + }; + const auto& cx = queue.context; + const auto stages = word(cx, 0x2d5); + if (!stages) return {}; + if ((*stages & 0x20u) != 0) { + if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, message); + } else if ((*stages & 4u) != 0) { + if (const auto parameters = word(cx, 0x2db)) { + const auto domain = *parameters & 3u; + const auto topology = (*parameters >> 5u) & 7u; + if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, message); + } + } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u && !(rectangles && (*primitive == 7u || *primitive == 17u))) { + return zeroMessage(0x242, *primitive, message); + } + return {}; +} + std::string singleSampleMsaaRejection(const QueueState& queue) { const auto& cx = queue.context; const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional { @@ -397,24 +420,25 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { } if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0 && !(*alphaToMask == AlphaToCoverageDithered && alphaToCoverageDisabled(cx))) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization"); - constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles"; - const auto stages = word(cx, 0x2d5); - if (!stages) return {}; - if ((*stages & 0x20u) != 0) { - if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, primitives); - } else if ((*stages & 4u) != 0) { - if (const auto parameters = word(cx, 0x2db)) { - const auto domain = *parameters & 3u; - const auto topology = (*parameters >> 5u) & 7u; - if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, primitives); - } - } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u) { - return zeroMessage(0x242, *primitive, primitives); - } + return nonTriangleRejection(queue, "MSAA_ENABLE on a single-sample target for primitives other than triangles"); +} + +std::string singleSampleDepthScanRejection(const QueueState& queue) { + if (auto reason = nonTriangleRejection(queue, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles", true); !reason.empty()) return reason; + const auto& cx = queue.context; + if (const auto eqaa = find(cx, 0x201); eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) return zeroMessage(0x201, eqaa->second, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)"); + if (PixelProgramSkipped(queue)) return {}; + constexpr std::uint32_t sampleIndex = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + const auto ena = find(cx, 0x1b3); + const auto addr = find(cx, 0x1b4); + if (ena != cx.end() && addr != cx.end() && (ena->second & addr->second & sampleIndex) != 0) return zeroMessage(0x1b3, ena->second, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index"); + if (const auto control = find(cx, 0x203); control != cx.end() && (control->second & 0x100u) != 0) return zeroMessage(0x203, control->second, "a single-sample scan into a multisampled depth target with a sample-mask export"); return {}; } constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; +constexpr std::uint32_t SampleZeroRefusedInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); +constexpr std::uint32_t PixelCenterPositionInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); bool coveredCentroidSupported(const QueueState& queue) { @@ -489,6 +513,24 @@ void decodeSampleLocations(const Registers& cx, State& state) { } } +bool rasterizesSampleZeroOnly(const Registers& cx, const State& state) { + if (state.samples <= 1 || state.alphaToCoverage || (read(cx, 0x292) & 1u) == 0) return false; + const auto inputs = read(cx, 0x1b3) & read(cx, 0x1b4); + const auto refused = state.perSampleShading ? ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage) : SampleZeroRefusedInputs; + const bool sampleIndexMovesPosition = (inputs & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0 && (inputs & PixelCenterPositionInputs) != 0; + return (inputs & refused) == 0 && !sampleIndexMovesPosition; +} + +void rasterizeSampleZero(State& state) { + std::array sampleZero{}; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) sampleZero[pixel * 8u] = state.sampleLocations[pixel * 8u]; + state.samples = 1; + state.perSampleShading = false; + state.sampleLocations = sampleZero; + state.customSampleLocations = std::any_of(sampleZero.begin(), sampleZero.end(), [](std::uint8_t bits) { return bits != 0; }); + state.sampleLocationGrid = sampleZero[8] != sampleZero[0] || sampleZero[16] != sampleZero[0] || sampleZero[24] != sampleZero[0]; +} + bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { colorControl &= ~1u; return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); @@ -711,7 +753,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, }; const bool alternate = swap == 1; const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; - if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12) return fail(); + if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12 && format != 16) return fail(); switch (format) { case 1: if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); @@ -779,6 +821,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16}; if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16}; return fail(); + case 16: + if ((swap != 0 && swap != 2) || number != unorm) return fail(); + return {VK_FORMAT_R5G6B5_UNORM_PACK16, 2, static_cast(swap == 2 ? 0xe4u : 0xc6u)}; case 17: if (swap != 0 || number != unorm) return fail(); return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u}; @@ -876,7 +921,10 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported"); const auto maxVertices = read(queue.context, 0x1ff); const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); - validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly"); + const auto outputType = read(queue.context, 0x29b); + validate(outputType == 2 || (outputType == 1 && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); + const auto outputSize = outputType == 1 ? 2u : 3u; + validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && verticesPerPrimitive >= outputSize, "unsupported geometry input or output assembly"); const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u; validate(vertices >= inputSize && maxVertices != 0 && (!vertexWorkgroup || maxVertices >= inputSize) && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output"); const auto inputStep = primitive == 5 || primitive == 6 ? 1u : inputSize; @@ -886,11 +934,11 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); - const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u; + const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - outputSize + 1u; validate(amplification != 0, "fast launch subgroup amplifies to no primitives"); - const auto maxPrimitives = primitives * amplification; + const auto maxPrimitives = outputSize == 2u && !fastLaunch ? std::max(primitives * amplification, maxVertices) : primitives * amplification; const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives}); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize, outputSize}; } return result; } @@ -912,10 +960,10 @@ bool DepthResummarizeDrawsNothing(const QueueState& queue) { std::string DepthMaintenanceRejection(const QueueState& queue) { if (DepthResummarizeDrawsNothing(queue)) return {}; const auto control = find(queue.context, 0x000); - if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {}; + if (control == queue.context.end() || (control->second & ~0x2fe3u) == 0) return {}; const auto depthControl = find(queue.context, 0x200); const bool colorMasks = find(queue.context, 0x8e) != queue.context.end() && find(queue.context, 0x8f) != queue.context.end(); - if ((control->second & ~0x2063u) == 0x10u && depthControl != queue.context.end() && (depthControl->second & 3u) == 0 && colorMasks && ColorWriteMask(queue.context) == 0) return {}; + if ((control->second & ~0x2fe3u) == 0x10u && depthControl != queue.context.end() && (depthControl->second & 3u) == 0 && colorMasks && ColorWriteMask(queue.context) == 0) return {}; return zeroMessage(0x000, control->second, "DB_RENDER_CONTROL depth copy, resummarize or decompress"); } @@ -1001,7 +1049,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto renderControl = find(cx, 0x000); const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; const auto tests = planeTests(cx, depthControl); - if (clear || (tests != 0 && depthSurfaceBound(cx))) { + if ((clear || tests != 0) && depthSurfaceBound(cx)) { decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; @@ -1031,15 +1079,33 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(ditheredAlphaSamplesSupported(cx), "dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"); Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported"); } - Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); + const bool depthKeepsSampleZero = result.depth && result.depth->samples == 1 && result.samples > 1; zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); - Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); + if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { + const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + result.customSampleLocations = true; + result.sampleLocations.fill(0); + result.sampleLocationGrid = false; + } if (result.samples == 1 && (read(cx, 0x292) & 1u) != 0) { for (std::uint32_t pixel = 0; pixel < 4; ++pixel) static_cast(read(cx, 0x2feu + pixel * 4u)); if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) throw std::runtime_error(reason); } zero(cx, 0x293, ScanControlMask, "primitive discard or out-of-order rasterization"); result.perSampleShading = result.samples > 1 && (read(cx, 0x293) & PixelShaderIteratesSamples) != 0; + if (depthKeepsSampleZero) { + Require(rasterizesSampleZeroOnly(cx, result), "the depth target's sample count differs from the rasterizer's"); + rasterizeSampleZero(result); + } + if (result.depth && result.depth->samples > 1 && result.samples == 1 && (read(cx, 0x292) & 1u) == 0) { + Require(!result.alphaToCoverage, "a single-sample scan into a multisampled depth target with alpha-to-coverage is unsupported"); + if (auto reason = singleSampleDepthScanRejection(queue); !reason.empty()) throw std::runtime_error(reason); + result.samples = result.depth->samples; + result.customSampleLocations = true; + result.sampleLocations.fill(0); + } + Require(!result.depth || depthKeepsSampleZero || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); @@ -1054,7 +1120,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 || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 2u); const auto lineOffset = (raster & 0x1800u) != 0 && !pointsOrLines ? 0x2000u : 0u; const auto rasterMode = raster & ~0x7u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u); if (rasterMode != 0 && rasterMode != 0x240u && rasterMode != 0x248u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "polygon mode, depth bias or nonstandard rasterization")); @@ -1105,6 +1171,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.customSampleLocations = true; result.sampleLocations.fill(0); } + if (color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { @@ -1249,6 +1316,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(addition(state.colorBlendOp) && addition(state.alphaBlendOp), "dual-source blending with a MIN or MAX operation is unsupported"); result.dualSourceBlend = true; } + if (const auto info = read(cx, 0x31c + slot * 0xfu); state.blendEnable && (info & 0x8000u) == 0 && ((info >> 8u) & 7u) <= 1u) { + char detail[80]; + std::snprintf(detail, sizeof(detail), " (CB_COLOR_INFO 0x%08x, CB_BLEND%u_CONTROL 0x%08x)", info, slot, blend); + throw std::runtime_error(std::string("AGC graphics: blending unclamped normalized color is not implemented") + detail); + } result.blends[color.exportIndex] = state; } for (std::uint32_t i = 0; i < 3; ++i) { @@ -1320,6 +1392,7 @@ std::array QuadSampleMasks(const State& state) { ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; + if (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 2u) return ShaderRecompiler::RasterPrimitive::Line; 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; @@ -1363,7 +1436,8 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { } GuestTextureResource SurfaceForTarget(const ColorTarget& color) { - Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident"); + static const bool residentLinear = std::getenv("APS5_NO_RESIDENT_LINEAR") == nullptr; + Require(residentLinear || 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; @@ -1404,10 +1478,9 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; - Require(allowMultisampleDcc || color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); - Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); + Require((info & 0x8000u) != 0 || number <= 1u || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); const auto slice = view & 0x1fffu; @@ -1424,7 +1497,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo 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, "mipmapped 3D color targets are unsupported"); + if (maxMip != 0) { + char message[384]; + std::snprintf(message, sizeof(message), "mipmapped 3D color targets are unsupported (slot %u, %ux%ux%u, viewMip %u, maxMip %u, slices %u..%u, CB_COLOR_INFO 0x%08x, ATTRIB2 0x%08x, ATTRIB3 0x%08x, VIEW 0x%08x)", slot, ((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u, color.depth, viewMip, maxMip, slice, lastSlice, info, attrib2, attrib3, view); + Require(false, message); + } 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; @@ -1504,6 +1581,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; + if (color.samples > 1) { + if (!allowMultisampleDcc) Require(color.tileMode == ColorTileMode::RenderTarget && color.dccAddress != 0 && color.dccPipeAligned && !volume && color.arrayLastSlice == 0 && slice == 0, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); + color.dccControl = read(cx, 0x31e + stride); + if (!allowMultisampleDcc) Require((color.dccControl & 0x3ff80u) == 0, "multisampled DCC color transforms or lossy modes are not implemented"); + } } else { static bool reported = false; if (!reported) { @@ -1624,11 +1706,11 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { const auto renderControl = find(cx, 0x000); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; - if (stencilClear) { + if (stencilClear && !depthPlanesAbsent(cx)) { std::uint32_t stencil = 0, view = 0; if ((value(cx, 0x011, stencil) && (stencil & 1u) == 0) || (value(cx, 0x002, view) && (view & 0x02000000u) != 0)) return require(false, "stencil clear requires a writable stencil plane"); } - if (depthClear) { + if (depthClear && !depthPlanesAbsent(cx)) { std::uint32_t z = 0, view = 0; if ((value(cx, 0x010, z) && (z & 3u) == 0) || (value(cx, 0x002, view) && (view & 0x01000000u) != 0)) return require(false, "depth clear requires a writable depth plane"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e71366c10b..ba62bdcde6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,8 +1,10 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -259,9 +261,9 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto& mips = geometry.mips; const auto arrayLayers = geometry.layers; const auto elementBytes = BytesPerElement(descriptor.format); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u dim=%d tile=%d format=%u vk=%d element=%u", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, arrayLayers, + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u dim=%d tile=%d format=%u vk=%d element=%u", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, arrayLayers, static_cast(descriptor.dimension), static_cast(descriptor.tileMode), descriptor.format, static_cast(vkFormat), elementBytes); - for (const auto& mip : mips) APS5_LOG_OUT(" mip %ux%u tiled=0x%llx+0x%llx linear=0x%llx+0x%llx blocksPerRow=%u pitch=%u tail=%d", mip.width, mip.height, static_cast(mip.tiledOffset), static_cast(mip.tiledSize), static_cast(mip.linearOffset), static_cast(mip.linearSize), mip.blocksPerRow, mip.pitchBytes, mip.tail ? 1 : 0); + for (const auto& mip : mips) AGC_LOG_DEFERRED(stdout, " mip %ux%u tiled=0x%llx+0x%llx linear=0x%llx+0x%llx blocksPerRow=%u pitch=%u tail=%d", mip.width, mip.height, static_cast(mip.tiledOffset), static_cast(mip.tiledSize), static_cast(mip.linearOffset), static_cast(mip.linearSize), mip.blocksPerRow, mip.pitchBytes, mip.tail ? 1 : 0); const auto guestBytes = geometry.guestBytes; Require(snapshot.size() == guestBytes, "texture snapshot size mismatch"); @@ -291,7 +293,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture"); + owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); { @@ -504,7 +506,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.imageType = VK_IMAGE_TYPE_2D; @@ -560,7 +562,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; @@ -582,7 +584,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory depth copy"); + owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory depth copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -747,7 +749,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& Require(source != nullptr && CanCopyFrom(*source, descriptor), "storage image does not match the sampled texture"); const auto vkFormat = SampledTextureFormat(context, descriptor.format); const auto geometry = DescribeSurface(descriptor); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u viewed from storage image (vk=%d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast(vkFormat)); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u viewed from storage image (vk=%d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast(vkFormat)); // Storage images stay in the general layout; the view samples them there. layout = VK_IMAGE_LAYOUT_GENERAL; const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -863,7 +865,13 @@ VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { VkImageView Texture::BaseLevelView() const { Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); std::lock_guard lock(baseLevelMutex); - if (!baseLevelView) Check(context.Function("vkCreateImageView")(context.device, &*baseLevelInfo, nullptr, &baseLevelView), "vkCreateImageView base level"); + if (!baseLevelView) { + auto info = *baseLevelInfo; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + info.pNext = &usage; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &baseLevelView), "vkCreateImageView base level"); + } return baseLevelView; } @@ -977,6 +985,10 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat) { } } +VkFormat HostStorageFormat(const Context& context, VkFormat format) { + return StorageFormatOrUndefined(context, format); +} + 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: @@ -1087,7 +1099,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, Require(mipLevel < descriptor.mipCount, "storage texture mip level is outside the texture"); const auto vkFormat = StorageFormatFor(context, ResolveTextureFormat(descriptor.format)); storageFormat = vkFormat; - APS5_LOG_OUT("StorageTexture address=0x%llx %ux%u mips=%u mip=%u layers=%u base=%u dim=%d tile=%d format=%u vk=%d", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, mipLevel, + AGC_LOG_DEFERRED(stdout, "StorageTexture address=0x%llx %ux%u mips=%u mip=%u layers=%u base=%u dim=%d tile=%d format=%u vk=%d", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, mipLevel, descriptor.depthOrLastArray, descriptor.baseArray, static_cast(descriptor.dimension), static_cast(descriptor.tileMode), descriptor.format, static_cast(vkFormat)); geometry = DescribeSurface(descriptor); mips = geometry.mips; @@ -1136,7 +1148,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); + memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); @@ -3171,6 +3183,12 @@ void StorageTexture::reconcilePending() { BumpPendingSerial(); } +bool StorageTexture::Dirty() const { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + return dirty; +} + void StorageTexture::Flush() { { auto& live = Live(); diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 1a2d0f5c58..ddb13df0a1 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -15,9 +15,9 @@ int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() { return 1; } -int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) { - (void)owner_handle; - return static_cast(0x8A6C9018); +int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t owner) { + (void)owner; + return 0; } int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(uint32_t frequency) { diff --git a/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp new file mode 100644 index 0000000000..c0b5dbeab4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp @@ -0,0 +1,29 @@ +#include "AllocationFailure.hpp" +#include +#include + +namespace { + +thread_local int allocationCountdown = -1; + +} + +void AgcDriver::Tests::SetAllocationFailureCountdown(int countdown) noexcept { + allocationCountdown = countdown; +} + +void* operator new(std::size_t bytes) { + if (allocationCountdown == 0) { + allocationCountdown = -1; + throw std::bad_alloc(); + } + if (allocationCountdown > 0) --allocationCountdown; + if (auto* allocation = std::malloc(bytes == 0 ? 1 : bytes)) return allocation; + throw std::bad_alloc(); +} + +void* operator new[](std::size_t bytes) { return ::operator new(bytes); } +void operator delete(void* allocation) noexcept { std::free(allocation); } +void operator delete[](void* allocation) noexcept { std::free(allocation); } +void operator delete(void* allocation, std::size_t) noexcept { std::free(allocation); } +void operator delete[](void* allocation, std::size_t) noexcept { std::free(allocation); } diff --git a/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp new file mode 100644 index 0000000000..947665af55 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_ALLOCATIONFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_ALLOCATIONFAILURE_HPP + +namespace AgcDriver::Tests { + +void SetAllocationFailureCountdown(int countdown) noexcept; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp new file mode 100644 index 0000000000..0325795a41 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp @@ -0,0 +1,337 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#endif + +namespace { + +using namespace std::chrono_literals; +using Clock = AgcDriver::AsyncLog::Clock; + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::string ReadAll(std::FILE* file) { + std::fflush(file); + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + return text; +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +void ClearLogEnvironment() { +#ifdef _WIN32 + _putenv("APS5_LOG_SYNC="); + _putenv("APS5_LOG_TIMESTAMPS="); +#else + unsetenv("APS5_LOG_SYNC"); + unsetenv("APS5_LOG_TIMESTAMPS"); +#endif +} + +void LockStream(std::FILE* file) { +#ifdef _WIN32 + _lock_file(file); +#else + flockfile(file); +#endif +} + +void UnlockStream(std::FILE* file) { +#ifdef _WIN32 + _unlock_file(file); +#else + funlockfile(file); +#endif +} + +class HeldStream { +public: + explicit HeldStream(std::FILE* file) : file(file) { + holder = std::thread([this] { + LockStream(this->file); + held.store(true); + while (!releaseRequested.load()) std::this_thread::sleep_for(1ms); + released.store(true); + UnlockStream(this->file); + }); + while (!held.load()) std::this_thread::sleep_for(1ms); + } + + HeldStream(const HeldStream&) = delete; + HeldStream& operator=(const HeldStream&) = delete; + + ~HeldStream() { + Release(); + holder.join(); + } + + void Release() { + releaseRequested.store(true); + } + + bool Released() const { + return released.load(); + } + +private: + std::FILE* file; + std::atomic held{false}; + std::atomic releaseRequested{false}; + std::atomic released{false}; + std::thread holder; +}; + +std::string LargeLine(std::size_t index, std::size_t bytes) { + std::string line = "large " + std::to_string(index) + " "; + line.resize(bytes - 1, 'x'); + line += '\n'; + return line; +} + +void OrderPerThread() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + std::string expected; + for (int i = 0; i < 2000; ++i) { + log.Defer(file, "deferred %d %s %llu\n", i, AgcDriver::LogLiteral{"literal"}, static_cast(i) * 3ull); + expected += "deferred " + std::to_string(i) + " literal " + std::to_string(static_cast(i) * 3ull) + "\n"; + const std::string plain = "plain " + std::to_string(i) + "\n"; + log.Write(file, plain.data(), plain.size()); + expected += plain; + } + log.Flush(); + Check(ReadAll(file) == expected, "deferred and written lines changed order or content"); + std::fclose(file); +} + +void LongLine() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + const std::string longText(3000, 'x'); + AgcDriver::AsyncLog::Get().Defer(file, "%s|%d\n", AgcDriver::LogLiteral{longText.c_str()}, 7); + AgcDriver::AsyncLog::Get().Flush(); + Check(ReadAll(file) == longText + "|7\n", "a line longer than the stack buffer changed"); + std::fclose(file); +} + +void Coalescing() { + auto& log = AgcDriver::AsyncLog::Get(); + const std::string line = "[gpu] coalescing test line\n"; + int printed = 0; + for (int i = 0; i < 50; ++i) printed += log.Admit(line) ? 1 : 0; + Check(printed == 3, "identical lines in one window were not limited to the first three"); + Check(log.Admit("[gpu] another line\n"), "a different line was suppressed"); +} + +void FlushWaitsForHeldStream() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + { + HeldStream held(file); + log.Defer(file, "held %d %s\n", 7, AgcDriver::LogLiteral{"line"}); + std::thread releaser([&] { + std::this_thread::sleep_for(2400ms); + held.Release(); + }); + log.Flush(); + const bool waited = held.Released(); + releaser.join(); + Check(waited, "Flush returned while the logger still waited for the stream"); + } + Check(ReadAll(file) == "held 7 line\n", "the line queued behind a held stream changed"); + std::fclose(file); +} + +void BoundedFlushGivesUp() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + Check(log.FlushFor(0ms), "FlushFor waited with nothing queued"); + { + HeldStream held(file); + log.Defer(file, "bounded %d\n", 1); + Check(!log.FlushFor(100ms), "FlushFor reported a line written while its stream was held"); + held.Release(); + Check(log.FlushFor(10s), "FlushFor missed the line written after the stream was released"); + } + Check(ReadAll(file) == "bounded 1\n", "the line queued behind a held stream changed"); + std::fclose(file); +} + +void FullQueueBlocksCaller() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + constexpr std::size_t lineBytes = 1024 * 1024; + constexpr std::size_t lines = 2 * AgcDriver::AsyncLog::Capacity / lineBytes + 4; + constexpr std::size_t filled = AgcDriver::AsyncLog::Capacity / lineBytes / 2; + std::atomic queued{0}; + bool blocked = false; + { + HeldStream held(file); + std::thread producer([&] { + for (std::size_t i = 0; i < lines; ++i) { + const std::string line = LargeLine(i, lineBytes); + log.Write(file, line.data(), line.size()); + queued.store(i + 1); + } + }); + auto last = queued.load(); + auto changedAt = Clock::now(); + const auto deadline = changedAt + 10s; + while (last < lines && Clock::now() < deadline && (last < filled || Clock::now() - changedAt < 500ms)) { + std::this_thread::sleep_for(5ms); + const auto now = queued.load(); + if (now != last) { + last = now; + changedAt = Clock::now(); + } + } + blocked = last >= filled && last < lines && Clock::now() - changedAt >= 500ms; + held.Release(); + producer.join(); + } + Check(blocked, "the caller was not blocked by a full queue"); + log.Flush(); + std::fflush(file); + std::rewind(file); + std::string line; + for (std::size_t i = 0; i < lines; ++i) { + const std::string expected = LargeLine(i, lineBytes); + line.resize(expected.size()); + Check(std::fread(line.data(), 1, line.size(), file) == line.size() && line == expected, "a line written through a full queue changed or went missing"); + } + Check(std::fgetc(file) == EOF, "a full queue wrote extra output"); + std::fclose(file); +} + +constexpr int ChildLines = 50; + +[[noreturn]] void EndChild(const std::string& mode, const char* output) { + std::FILE* file = std::fopen(output, "wb"); + Check(file != nullptr, "child output file"); +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#else + const rlimit noCore{0, 0}; + setrlimit(RLIMIT_CORE, &noCore); +#endif + std::thread([] { + std::this_thread::sleep_for(8s); + std::_Exit(99); + }).detach(); + AgcDriver::InstallAsyncLog_nid_no_patch(); + auto& log = AgcDriver::AsyncLog::Get(); + HeldStream held(file); + for (int i = 0; i < ChildLines; ++i) { + log.Defer(file, "deferred %d\n", i); + WriteLogLine(file, "plain %d\n", i); + } + std::thread releaser([&] { + std::this_thread::sleep_for(100ms); + if (mode != "blocked") held.Release(); + }); + if (mode == "terminate") std::terminate(); +#ifdef _WIN32 + if (mode == "fault") RaiseException(EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, nullptr); +#else + if (mode == "raise") { + std::raise(SIGABRT); + std::_Exit(0); + } +#endif + std::abort(); +} + +std::string Quote(const std::string& text) { + return "\"" + text + "\""; +} + +void LinesSurviveEnd(const std::string& self, const std::string& mode) { + const auto output = std::filesystem::temp_directory_path() / ("aps5-async-log-" + mode + "-" + std::to_string(Clock::now().time_since_epoch().count()) + ".txt"); + std::string command = Quote(self) + " child " + mode + " " + Quote(output.string()); +#ifdef _WIN32 + command = Quote(command); +#endif + const auto start = Clock::now(); + const int status = std::system(command.c_str()); + const auto lived = Clock::now() - start; + std::error_code error; + const bool started = std::filesystem::exists(output, error); + const std::string text = started ? ReadFile(output) : std::string(); + std::filesystem::remove(output, error); + Check(started, "the child did not start"); + Check(status != 0, "the child did not end abnormally"); + Check(lived < 6s, "the child took too long to end"); + if (mode == "blocked") { + Check(lived >= 1900ms, "the child ended before the flush gave up"); + Check(text.empty(), "the child wrote through a held stream"); + return; + } + std::string expected; + for (int i = 0; i < ChildLines; ++i) expected += "deferred " + std::to_string(i) + "\nplain " + std::to_string(i) + "\n"; + Check(text == expected, "lines queued before the process ended were lost or changed"); +} + +} + +int main(int argc, char** argv) { + try { + ClearLogEnvironment(); + if (argc == 4 && std::strcmp(argv[1], "child") == 0) EndChild(argv[2], argv[3]); + if (argc == 2 && std::strcmp(argv[1], "fatal") == 0) { + LinesSurviveEnd(argv[0], "abort"); + LinesSurviveEnd(argv[0], "terminate"); +#ifdef _WIN32 + LinesSurviveEnd(argv[0], "fault"); +#else + LinesSurviveEnd(argv[0], "raise"); +#endif + LinesSurviveEnd(argv[0], "blocked"); + return 0; + } + OrderPerThread(); + LongLine(); + Coalescing(); + FlushWaitsForHeldStream(); + BoundedFlushGivesUp(); + FullQueueBlocksCaller(); + } catch (const std::exception& error) { + std::cerr << error.what() << std::endl; + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index cf54e315dd..312d83bede 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -1,10 +1,120 @@ #include "BdaShader.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" +#include +#include +#include +#include +#include +#include #include namespace { +struct ScanWords { + std::vector words; + void Op(spv::Op opcode, std::initializer_list operands) { + words.push_back((static_cast(operands.size() + 1u) << 16u) | static_cast(opcode)); + words.insert(words.end(), operands.begin(), operands.end()); + } + void OpString(spv::Op opcode, std::initializer_list operands, const char* text) { + std::vector packed((std::strlen(text) + 4u) / 4u, 0u); + std::memcpy(packed.data(), text, std::strlen(text)); + words.push_back((static_cast(operands.size() + packed.size() + 1u) << 16u) | static_cast(opcode)); + words.insert(words.end(), operands.begin(), operands.end()); + words.insert(words.end(), packed.begin(), packed.end()); + } +}; + +enum ScanId : std::uint32_t { + IdVoid = 1, IdVoidFunction, IdUint, IdUlong, IdUintPointer, IdAddress, IdOne, IdDeviceScope, IdNoSemantics, IdPointerPointer, IdPointerFunction, IdBool, IdTrue, IdFloat, IdFloatPointer, IdGlsl, IdFunctionUintPointer, IdUntypedPointer, IdFloatOne, IdMain, IdMainLabel, IdPointer, IdHelper, IdHelperParameter, IdHelperLabel, IdBody, +}; + +bool ScanStores(const std::function& main, const std::function& after = {}) { + ScanWords module{{spv::MagicNumber, 0x00010500u, 0u, 200u, 0u}}; + module.Op(spv::OpCapability, {spv::CapabilityShader}); + module.Op(spv::OpCapability, {spv::CapabilityInt64}); + module.Op(spv::OpCapability, {spv::CapabilityPhysicalStorageBufferAddresses}); + module.OpString(spv::OpExtInstImport, {IdGlsl}, "GLSL.std.450"); + module.Op(spv::OpMemoryModel, {spv::AddressingModelPhysicalStorageBuffer64, spv::MemoryModelGLSL450}); + module.OpString(spv::OpEntryPoint, {spv::ExecutionModelGLCompute, IdMain}, "main"); + module.Op(spv::OpTypeVoid, {IdVoid}); + module.Op(spv::OpTypeFunction, {IdVoidFunction, IdVoid}); + module.Op(spv::OpTypeInt, {IdUint, 32u, 0u}); + module.Op(spv::OpTypeInt, {IdUlong, 64u, 0u}); + module.Op(spv::OpTypeFloat, {IdFloat, 32u}); + module.Op(spv::OpTypeBool, {IdBool}); + module.Op(spv::OpTypePointer, {IdUintPointer, spv::StorageClassPhysicalStorageBuffer, IdUint}); + module.Op(spv::OpTypePointer, {IdPointerPointer, spv::StorageClassPhysicalStorageBuffer, IdUintPointer}); + module.Op(spv::OpTypePointer, {IdFloatPointer, spv::StorageClassPhysicalStorageBuffer, IdFloat}); + module.Op(spv::OpTypePointer, {IdPointerFunction, spv::StorageClassFunction, IdUint}); + module.Op(spv::OpTypeUntypedPointerKHR, {IdUntypedPointer, spv::StorageClassPhysicalStorageBuffer}); + module.Op(spv::OpTypeFunction, {IdFunctionUintPointer, IdVoid, IdUintPointer}); + module.Op(spv::OpConstant, {IdUlong, IdAddress, 0x1000u, 0u}); + module.Op(spv::OpConstant, {IdUint, IdOne, 1u}); + module.Op(spv::OpConstant, {IdUint, IdDeviceScope, spv::ScopeDevice}); + module.Op(spv::OpConstant, {IdUint, IdNoSemantics, 0u}); + module.Op(spv::OpConstant, {IdFloat, IdFloatOne, 0x3f800000u}); + module.Op(spv::OpConstantTrue, {IdBool, IdTrue}); + module.Op(spv::OpFunction, {IdVoid, IdMain, spv::FunctionControlMaskNone, IdVoidFunction}); + module.Op(spv::OpLabel, {IdMainLabel}); + module.Op(spv::OpConvertUToPtr, {IdUintPointer, IdPointer, IdAddress}); + main(module); + module.Op(spv::OpReturn, {}); + module.Op(spv::OpFunctionEnd, {}); + if (after) after(module); + return AgcDriver::Graphics::SpirvStoresThroughAddresses(module.words); +} + +void RunAddressStoreScanTests() { + using AgcDriver::Graphics::Require; + Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpLoad, {IdUint, IdBody, IdPointer}); }), "store scan: a load through a physical pointer was taken for a store"); + Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicLoad, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics}); }), "store scan: an atomic load was taken for a store"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpStore, {IdPointer, IdOne}); }), "store scan: a store was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicStore, {IdPointer, IdDeviceScope, IdNoSemantics, IdOne}); }), "store scan: an atomic store was missed"); + for (const auto opcode : {spv::OpAtomicExchange, spv::OpAtomicIAdd, spv::OpAtomicISub, spv::OpAtomicSMin, spv::OpAtomicUMin, spv::OpAtomicSMax, spv::OpAtomicUMax, spv::OpAtomicAnd, spv::OpAtomicOr, spv::OpAtomicXor}) { + Require(ScanStores([opcode](ScanWords& m) { m.Op(opcode, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics, IdOne}); }), "store scan: atomic opcode " + std::to_string(static_cast(opcode)) + " was missed"); + } + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicIIncrement, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics}); }), "store scan: an atomic increment was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicCompareExchange, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics, IdNoSemantics, IdOne, IdOne}); }), "store scan: an atomic compare-exchange was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpConvertUToPtr, {IdPointerPointer, IdBody, IdAddress}); + m.Op(spv::OpLoad, {IdUintPointer, IdBody + 1u, IdBody}); + m.Op(spv::OpStore, {IdBody + 1u, IdOne}); + }), "store scan: a store through a pointer loaded from guest memory was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpSelect, {IdUintPointer, IdBody, IdTrue, IdPointer, IdPointer}); + m.Op(spv::OpStore, {IdBody, IdOne}); + }), "store scan: a store through a selected pointer was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpFunctionCall, {IdVoid, IdBody, IdHelper, IdPointer}); }, [](ScanWords& m) { + m.Op(spv::OpFunction, {IdVoid, IdHelper, spv::FunctionControlMaskNone, IdFunctionUintPointer}); + m.Op(spv::OpFunctionParameter, {IdUintPointer, IdHelperParameter}); + m.Op(spv::OpLabel, {IdHelperLabel}); + m.Op(spv::OpStore, {IdHelperParameter, IdOne}); + m.Op(spv::OpReturn, {}); + m.Op(spv::OpFunctionEnd, {}); + }), "store scan: a store through a pointer parameter of a called function was missed"); + Require(!ScanStores([](ScanWords& m) { + m.Op(spv::OpVariable, {IdPointerFunction, IdBody, spv::StorageClassFunction}); + m.Op(spv::OpCopyMemory, {IdBody, IdPointer}); + }), "store scan: a copy out of guest memory was taken for a store"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpVariable, {IdPointerFunction, IdBody, spv::StorageClassFunction}); + m.Op(spv::OpCopyMemory, {IdPointer, IdBody}); + }), "store scan: a copy into guest memory was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpConvertUToPtr, {IdUntypedPointer, IdBody, IdAddress}); + m.Op(spv::OpStore, {IdBody, IdOne}); + }), "store scan: a store through an untyped physical pointer was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpConvertUToPtr, {IdFloatPointer, IdBody, IdAddress}); + m.Op(spv::OpExtInst, {IdFloat, IdBody + 1u, IdGlsl, GLSLstd450Modf, IdFloatOne, IdBody}); + }), "store scan: an extended instruction writing through a physical pointer was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpCooperativeMatrixStoreKHR, {IdPointer, IdOne, IdNoSemantics}); }), "store scan: a cooperative matrix store was missed"); +} + + template void reject(TAction action, const char* expected) { try { action(); } @@ -19,6 +129,21 @@ void reject(TAction action, const char* expected) { void RunBdaContractTests() { using namespace ShaderRecompiler; + using AgcDriver::Graphics::SpirvStoresThroughAddresses; + RunAddressStoreScanTests(); + AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaTestShader(0x1000, 32)), "a BDA read was taken for a store through an address"); + AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaDwordReadTestShader(0x1000, 4, false, false)), "BDA dword reads were taken for a store through an address"); + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaStoreTestShader(0x1000, 7, 32, false)), "a BDA store was not seen"); + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaWriteTestShader(0x1000, 7, 32, false)), "a BDA write was not seen"); + const std::array stored{7u, 9u}; + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaDwordWriteTestShader(0x1000, static_cast(stored.size()), stored.data())), "BDA dword writes were not seen"); + const std::array headerOnly{spv::MagicNumber, 0x00010300u, 0u, 1u, 0u}; + AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(headerOnly), "a module without physical pointers was taken for one storing through addresses"); + const std::array truncated{spv::MagicNumber, 0u}; + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(truncated), "an unreadable module was taken for one without stores"); + auto unparsed = MakeBdaStoreTestShader(0x1000, 7, 32, false); + unparsed[5] |= 0xffff0000u; + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(unparsed), "a module with an instruction past its end was taken for one without stores"); const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; SpirvTargetOptions target{0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2fe20f100c..dd9ef7ac25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -550,6 +550,137 @@ void importJoinTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +class ImportBudgetRanges { +public: + static constexpr std::size_t piece = 65536; + static constexpr std::size_t bytes = 3 * piece; + + explicit ImportBudgetRanges(const Context& context) : importing(context) { + block = static_cast(::operator new(4 * bytes, std::align_val_t{piece})); + importing.hostImportAlignment = piece; + ClearHostImports(importing.device); + SetHostImportWindow(2 * piece); + SetHostImportRangeWindow(0); + SetHostImportBudget(2 * bytes); + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 4; ++index) { + std::memset(block + index * bytes, static_cast(0x31 + index), bytes); + mutation.Add(block + index * bytes, bytes, true, false, true); + } + } + + ~ImportBudgetRanges() { + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 4; ++index) mutation.Remove(block + index * bytes); + } + SetHostImportBudget(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{piece}); + } + + ImportBudgetRanges(const ImportBudgetRanges&) = delete; + ImportBudgetRanges& operator=(const ImportBudgetRanges&) = delete; + + std::uintptr_t Address(std::size_t index) const { return reinterpret_cast(block + index * bytes); } + + void CheckUnchanged() const { + for (std::size_t index = 0; index < 4; ++index) { + const auto expected = static_cast(0x31 + index); + Require(std::all_of(block + index * bytes, block + (index + 1) * bytes, [expected](std::byte value) { return value == expected; }), "import budget probes changed guest bytes"); + } + } + + Context importing; + +private: + std::byte* block = nullptr; +}; + +void importCoverageHintBudgetTests(const Context& context, const BdaTestAccess& access) { + for (const bool acquired : {true, false}) { + ImportBudgetRanges ranges(context); + const auto a = ranges.Address(0); + const auto c = ranges.Address(2); + const auto d = ranges.Address(3); + const auto* first = HostImportFor(ranges.importing, a, 16); + Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "coverage budget fixture did not import its first range"); + const auto firstBuffer = first->buffer; + const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false); + for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "coverage budget fixture did not import its hot range"); + const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false); + Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "coverage budget fixture has unexpected import ownership or usage"); + { + GuestBufferMemory memory(ranges.importing); + memory.AddReadable(c, 32); + memory.UploadPrepare(false); + if (acquired) { + const auto* held = HostImportFor(ranges.importing, a, 16); + Require(held != nullptr && held->buffer == firstBuffer, "a genuine import acquisition replaced the retained range"); + } else { + Require(HostImportCovers(ranges.importing, a, 16), "a coverage hint missed the existing range"); + } + memory.UploadFinish(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(c, 32, adjustment); + Require(view.range == 32 + adjustment, "a disjoint descriptor has an incorrect extent"); + Require(HostImportSerial(ranges.importing, d, 16, false) == hotSerial && HostImportLiveBytes() == 2 * ranges.bytes, "a coverage probe displaced the hot import or changed the budget"); + if (acquired) { + Require(view.buffer != firstBuffer, "a disjoint descriptor uses the retained range's import"); + Require(HostImportSerial(ranges.importing, a, 16, false) == firstSerial && HostImportSerial(ranges.importing, c, 16, false) == 0, "a genuine import acquisition did not protect its buffer from eviction"); + Require(memory.CopiedBytes() == 32, "a disjoint descriptor at the budget was not copied while an import was held"); + const auto copied = access.bytes(view.buffer); + const auto offset = static_cast(view.offset + adjustment); + Require(offset + 32 <= copied.size() && std::memcmp(copied.data() + offset, reinterpret_cast(c), 32) == 0, "the descriptor copied behind a held import lost its guest bytes"); + } else { + const auto importedSerial = HostImportSerial(ranges.importing, c, 16, false); + Require(HostImportSerial(ranges.importing, a, 16, false) == 0 && importedSerial != 0 && importedSerial != firstSerial, "a coverage hint prevented eviction of an unused import"); + const auto* imported = ExistingHostImportFor(ranges.importing, c, 32); + Require(imported != nullptr && imported->base == c && imported->bytes == ranges.bytes && view.buffer == imported->buffer && view.offset + adjustment == c - imported->base, "a descriptor does not bind the import obtained after a coverage hint"); + Require(memory.CopiedBytes() == 0, "a coverage hint forced the disjoint imported descriptor to copy"); + } + memory.WriteBack(); + } + ranges.CheckUnchanged(); + } +} + +void failedImportJoinProbeBudgetTests(const Context& context, const BdaTestAccess& access) { + ImportBudgetRanges ranges(context); + const auto a = ranges.Address(0); + const auto crossing = ranges.Address(1) - 16; + const auto c = ranges.Address(2); + const auto d = ranges.Address(3); + const auto* first = HostImportFor(ranges.importing, a, 16); + Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "failed join budget fixture did not import its first range"); + const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false); + for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "failed join budget fixture did not import its hot range"); + const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false); + Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "failed join budget fixture has unexpected import ownership or usage"); + { + GuestBufferMemory memory(ranges.importing); + memory.AddReadable(crossing, 32); + memory.AddReadable(c, 32); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto copiedView = memory.Descriptor(crossing, 32, adjustment); + const auto copied = access.bytes(copiedView.buffer); + const auto offset = static_cast(copiedView.offset + adjustment); + Require(copiedView.range == 32 + adjustment && offset + 32 <= copied.size() && std::memcmp(copied.data() + offset, reinterpret_cast(crossing), 32) == 0, "the refused joined descriptor lost either side's guest bytes"); + Require(HostImportSerial(ranges.importing, crossing, 32, false) == 0, "a join over the available budget unexpectedly produced an import"); + const auto importedSerial = HostImportSerial(ranges.importing, c, 16, false); + Require(HostImportSerial(ranges.importing, a, 16, false) == 0 && importedSerial != 0 && importedSerial != firstSerial, "a failed join endpoint probe prevented eviction of an unused import"); + const auto* imported = ExistingHostImportFor(ranges.importing, c, 32); + const auto view = memory.Descriptor(c, 32, adjustment); + Require(imported != nullptr && imported->base == c && imported->bytes == ranges.bytes && view.buffer == imported->buffer && view.offset + adjustment == c - imported->base && view.range == 32 + adjustment, "the descriptor after a failed join does not bind its complete import"); + Require(memory.CopiedBytes() == 32 && HostImportLiveBytes() == 2 * ranges.bytes && HostImportSerial(ranges.importing, d, 16, false) == hotSerial, "a failed join forced a second copy or displaced the hot import"); + memory.WriteBack(); + } + ranges.CheckUnchanged(); +} + // One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the // test and reset after it. class WindowedRange { @@ -782,6 +913,151 @@ void spaceReadAcrossImportsWrittenTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t pieces = 6; + void* block = ::operator new(pieces * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + for (std::size_t index = 0; index < pieces; ++index) std::memset(guest + index * piece, static_cast(0x10 + index), piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, range == guest + 3 * piece, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < pieces; ++index) change(guest + index * piece, true); + const auto second = first + window; + const auto third = first + 2 * window; + const std::uint64_t lower = second - 256; + const std::uint64_t lowerBytes = 512; + const std::uint64_t upper = second + 128; + const std::uint64_t upperBytes = third + 256 - upper; + for (int build = 0; build < 3; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(lower, lowerBytes); + leased.AddReadable(upper, upperBytes); + leased.UploadPrepare(true); + if (build == 2) ClearHostImports(importing.device); + leased.UploadFinish(true); + for (const auto& [address, bytes] : {std::pair{lower, lowerBytes}, std::pair{upper, upperBytes}}) { + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(address, bytes, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == bytes + adjustment, "a read-only V# over imports, merged with another one, has no descriptor of its size"); + const auto mapped = access.bytes(view.buffer); + const auto at = static_cast(view.offset + adjustment); + const auto expected = [&](std::uint64_t guestAddress) { return std::byte{static_cast(0x10 + (guestAddress - first) / piece)}; }; + Require(at + bytes <= mapped.size() && mapped[at] == expected(address) && mapped[at + bytes - 1] == expected(address + bytes - 1), "a read-only V# over imports, merged with another one, maps other bytes than its own"); + } + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build > 0) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap && after.dissolvedImports == before.dissolvedImports + (build == 2 ? 1u : 0u), "read-only V#s over imports, merged with each other, left the cached address space other than by the import change"); + } + for (std::size_t index = 0; index < pieces; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +void spaceWriteMergedPastImportTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t pieces = 5; + void* block = ::operator new(pieces * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto second = first + window; + const auto outside = first + 2 * window; + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, range == guest + 3 * piece, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index + 1 < pieces; ++index) change(guest + index * piece, true); + const std::uint64_t written = outside + 512; + for (int build = 0; build < 2; ++build) { + std::memset(guest, 0x21, pieces * piece); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 256, window + 512); + leased.AddReadable(outside + 128, 640); + leased.AddWritable(written, 256); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(written, 256, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 256 + adjustment, "a written V# merged with a read-only V# past an import has no descriptor"); + auto mapped = access.bytes(view.buffer); + const auto at = static_cast(view.offset + adjustment); + Require(at + 256 <= mapped.size() && mapped[at] == std::byte{0x21} && mapped[at + 255] == std::byte{0x21}, "a written V# merged with a read-only V# past an import maps other bytes than its own"); + std::memset(mapped.data() + at, 0x7c, 256); + Require(leased.HasCopiedWrites(), "a written V# merged with a read-only V# past an import is not stored back"); + leased.WriteBack(); + const auto offset = static_cast(written - first); + Require(guest[offset] == 0x7c && guest[offset + 255] == 0x7c && guest[offset - 1] == 0x21 && guest[offset + 256] == 0x21, "a write through a V# merged with a read-only V# past an import did not land on its own bytes"); + } + for (std::size_t index = 0; index + 1 < pieces; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +void spaceReadOverWritableImportTableTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto boundary = first + window; + std::memset(guest, 0x5a, 3 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, range != guest + 2 * piece, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + const auto lookup = [](const std::vector& ranges, std::uint64_t address) { + const auto found = std::upper_bound(ranges.begin(), ranges.end(), address, [](std::uint64_t value, const auto& range) { return value < range.begin; }); + return found == ranges.begin() ? ranges.end() : std::prev(found); + }; + for (int build = 0; build < 3; ++build) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(boundary - 256, 512); + leased.UploadPrepare(true); + if (build == 2) ClearHostImports(importing.device); + leased.UploadFinish(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(boundary - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a read-only V# across a writable and a read-only import has no descriptor"); + const auto ranges = leased.AddressRanges(); + Require(std::adjacent_find(ranges.begin(), ranges.end(), [](const auto& left, const auto& right) { return right.begin < left.end; }) == ranges.end(), "the BDA table of a build with a read-only V# across imports has overlapping ranges"); + for (const auto address : {first + 16, boundary - 256, boundary - 128, boundary - 16}) { + const auto found = lookup(ranges, address); + Require(found != ranges.end() && address + 16 <= found->end && (found->permissions & ShaderRecompiler::BdaAbi::Write) != 0u, "a BDA store into a writable import resolves to the read-only copy of a V# across it"); + } + for (const auto address : {boundary, boundary + 64, boundary + piece - 32}) { + const auto found = lookup(ranges, address); + Require(found != ranges.end() && address + 16 <= found->end && (found->permissions & ShaderRecompiler::BdaAbi::Read) != 0u, "a BDA read past a read-only V# across imports finds no range"); + } + leased.WriteBack(); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1785,7 +2061,12 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importWindowTests(context); spaceReadAcrossImportsTests(context, access); spaceReadAcrossImportsWrittenTests(context); + spaceReadsAcrossImportsMergedTests(context, access); + spaceWriteMergedPastImportTests(context, access); + spaceReadOverWritableImportTableTests(context); importJoinTests(context); + importCoverageHintBudgetTests(context, access); + failedImportJoinProbeBudgetTests(context, access); importWholeRangeOverWindowTests(context); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index f0ac0664ab..7863a8ce05 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -1,21 +1,27 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "AllocationFailure.hpp" #include #include +#include #include #include #include #include #include +#include #include #include #include #include using namespace AgcDriver::Graphics; +using AgcDriver::Tests::SetAllocationFailureCountdown; namespace { +thread_local int failAfterMap = -1; + int failures = 0; void Expect(bool condition, const std::string& what) { @@ -44,6 +50,11 @@ struct MockDevice { VkDeviceSize limit = 0; VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY; std::uint64_t attempts = 0; + VkResult mapResult = VK_SUCCESS; + VkResult bindResult = VK_SUCCESS; + unsigned binds = 0; + unsigned maps = 0; + unsigned unmaps = 0; }; MockDevice mock; @@ -82,17 +93,23 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca } VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + ++mock.binds; + if (mock.bindResult != VK_SUCCESS) return mock.bindResult; if (offset + mock.sizes.at(buffer) > mock.memoryBytes.at(memory)) return VK_ERROR_OUT_OF_DEVICE_MEMORY; mock.bound[buffer] = {memory, offset}; return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize, VkDeviceSize, VkMemoryMapFlags, void** data) { + if (mock.mapResult != VK_SUCCESS) return mock.mapResult; *data = mock.hostMemory.at(memory).data(); + ++mock.maps; + SetAllocationFailureCountdown(failAfterMap); + failAfterMap = -1; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {} +VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) { ++mock.unmaps; } VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed"); @@ -210,6 +227,31 @@ void SizeClassesAndDirections() { Expect(mock.sizes.at(writeBack.Handle()) == 10 * MiB + 512 * 1024, "a reused device buffer kept the VkBuffer of the earlier size"); } +void SmallDeviceRetention() { + mock = MockDevice{}; + { + auto context = mockContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + const auto deviceBudget = BufferPool::DeviceBudget(context.memory); + const auto bytes = static_cast((deviceBudget != 0 ? deviceBudget : 512 * MiB) / 2) + MiB; + VkDeviceMemory small = VK_NULL_HANDLE; + { + DeviceBuffer buffer(context, 256, usage); + small = MemoryOf(buffer.Handle()); + } + std::vector> large; + for (int i = 0; i < 2; ++i) large.push_back(std::make_unique(context, bytes, usage)); + large.clear(); + Expect(mock.frees != 0, "large device buffers did not exercise pool budget eviction"); + const auto made = mock.allocations; + const bool isolated = std::getenv("APS5_BUFFER_POOL_SHARED") == nullptr; + DeviceBuffer again(context, 256, usage); + Expect((MemoryOf(again.Handle()) == small) == isolated, "small device buffer retention did not follow the separate or shared pool budget"); + Expect(mock.allocations == made + (isolated ? 0 : 1), "large device buffer churn changed small buffer allocation reuse"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "device pool tier retention leaked Vulkan objects"); +} + void LargerClass() { mock = MockDevice{}; auto context = mockContext(); @@ -326,6 +368,66 @@ void SmallDeviceClasses() { Expect(nextArgs.Handle() == tiny && mock.allocations == made, "a 20-byte device request did not reuse the retained 256-byte buffer of its class"); } +void SlabMetadataFailure() { + unsigned rejected = 0; + for (int point = 0; point < 16; ++point) { + mock = MockDevice{}; + bool failed = false; + { + const auto context = mockContext(); + BufferPool pool(context); + SlabSlot slot{}; + failAfterMap = point; + try { + slot = pool.TakeSlot(context, 1, 512, false); + } catch (const std::bad_alloc&) { + failed = true; + ++rejected; + } + SetAllocationFailureCountdown(-1); + failAfterMap = -1; + if (failed) { + Expect(mock.allocations == 1, "metadata failure did not follow a successful Vulkan allocation"); + Expect(mock.frees == 1 && mock.liveBytes == 0, "metadata failure leaked the slab's device memory at allocation " + std::to_string(point)); + Expect(mock.maps == 1 && mock.unmaps == 1, "metadata failure did not unmap its slab"); + Expect(pool.SlabBlocks() == 0, "metadata failure published an unusable slab"); + slot = pool.TakeSlot(context, 1, 512, false); + } + Expect(slot.mapping != nullptr, "slab retry did not return mapped memory"); + pool.PutSlot(slot.memory, slot.offset); + } + Expect(mock.liveBytes == 0 && mock.allocations == mock.frees, "metadata failure retained device memory after pool destruction"); + if (!failed) { + Expect(rejected != 0, "slab metadata allocation failures were not exercised"); + std::printf("slab metadata failures checked: %u\n", rejected); + return; + } + } + Expect(false, "slab allocation never reached the success control"); +} + +void SlabMapFailure() { + mock = MockDevice{}; + { + const auto context = mockContext(); + BufferPool pool(context); + mock.mapResult = VK_ERROR_MEMORY_MAP_FAILED; + bool failed = false; + try { + static_cast(pool.TakeSlot(context, 1, 512, false)); + } catch (const std::runtime_error& error) { + failed = std::strstr(error.what(), "vkMapMemory buffer slab") != nullptr; + } + Expect(failed, "the slab mapping failure was not reported"); + Expect(mock.allocations == 1 && mock.frees == 1 && mock.liveBytes == 0, "a failed slab mapping did not release exactly its allocation"); + Expect(mock.maps == 0 && mock.unmaps == 0 && pool.SlabBlocks() == 0, "a failed mapping published a slab or unmapped invalid memory"); + mock.mapResult = VK_SUCCESS; + const auto slot = pool.TakeSlot(context, 1, 512, false); + pool.PutSlot(slot.memory, slot.offset); + } + Expect(mock.liveBytes == 0 && mock.allocations == mock.frees && mock.maps == mock.unmaps, "a failed mapping made a later slab leak"); +} + void Slabs() { Expect(BufferPool::SlabEligible(512, 256, 512, 64), "a size class with a smaller alignment and atom was refused a slab"); Expect(!BufferPool::SlabEligible(512, 1024, 512, 64), "a size class below its alignment was given a slab"); @@ -496,6 +598,101 @@ void AllocationFailure(VkResult initial, VkResult final, bool cached) { Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource"); } +struct ReclaimMock { + VkDeviceMemory external = VK_NULL_HANDLE; + VkDeviceSize externalBytes = 0; + std::uint64_t requests = 0; + VkDeviceSize asked = 0; +}; + +ReclaimMock reclaimMock; + +VkDeviceMemory ExternalAllocation(VkDeviceSize bytes) { + VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + info.allocationSize = bytes; + VkDeviceMemory memory = VK_NULL_HANDLE; + Expect(mockAllocateMemory(VK_NULL_HANDLE, &info, nullptr, &memory) == VK_SUCCESS, "the external allocation of the reclaim test failed"); + reclaimMock.external = memory; + reclaimMock.externalBytes = bytes; + return memory; +} + +VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) { + ++reclaimMock.requests; + reclaimMock.asked = bytes; + if (reclaimMock.external == VK_NULL_HANDLE) return 0; + mockFreeMemory(VK_NULL_HANDLE, reclaimMock.external, nullptr); + reclaimMock.external = VK_NULL_HANDLE; + return reclaimMock.externalBytes; +} + +void CacheReclaimRecovery() { + Expect(BufferPool::ReclaimTarget(4 * MiB) == 260 * MiB, "a 4 MiB request does not ask the caches for 256 MiB on top of itself"); + Expect(BufferPool::ReclaimTarget(512 * MiB) == 1024 * MiB, "a 512 MiB request does not ask the caches for twice its size"); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + { + auto context = mockContext(); + context.cacheReclaim = mockCacheReclaim; + { DeviceBuffer idle(context, 2 * MiB, usage); } + ExternalAllocation(10 * MiB); + mock.limit = 12 * MiB; + const auto before = mock.attempts; + try { + DeviceBuffer recovered(context, 4 * MiB, usage); + Expect(mock.attempts == before + 3, "an allocation the pool's own slots could not satisfy did not retry once after the trim and once after the cache release"); + Expect(reclaimMock.requests == 1 && reclaimMock.asked == 260 * MiB, "the caches were not asked once for the request plus its reserve"); + Expect(mock.frees == 2 && mock.liveBytes == 4 * MiB, "the trim or the cache release did not free its memory"); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "cache release recovery leaked a resource"); + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + { + auto context = mockContext(); + context.cacheReclaim = mockCacheReclaim; + ExternalAllocation(10 * MiB); + mock.limit = 12 * MiB; + const auto before = mock.attempts; + try { + DeviceBuffer recovered(context, 4 * MiB, usage); + Expect(mock.attempts == before + 2 && reclaimMock.requests == 1, "an empty pool did not go straight to the cache release and retry once"); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release alone did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "cache release recovery with an empty pool leaked a resource"); + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + { + auto context = mockContext(); + context.cacheReclaim = mockCacheReclaim; + mock.limit = 2 * MiB; + const auto before = mock.attempts; + std::string failure; + try { + DeviceBuffer buffer(context, 4 * MiB, usage); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(reclaimMock.requests == 1 && mock.attempts == before + 1, "a cache release that freed nothing was retried after"); + Expect(failure.find("Vulkan result -2") != std::string::npos && failure.find("released=0") != std::string::npos, "the failure after an empty cache release lost its result or the released bytes"); + mock.results = {VK_ERROR_DEVICE_LOST}; + mock.limit = 0; + failure.clear(); + try { + DeviceBuffer buffer(context, 4 * MiB, usage); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(reclaimMock.requests == 1 && failure.find("Vulkan result -4") != std::string::npos, "a device loss asked the caches for memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "the failed cache release leaked a resource"); +} + void SuccessfulAllocationKeepsPool() { mock = MockDevice{}; { @@ -528,16 +725,55 @@ void PartialSlabDoesNotRetry() { Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource"); } +void DeviceBindingFailures() { + constexpr auto bytes = 3 * MiB; + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + mock = MockDevice{}; + { + auto context = mockContext(); + mock.bindResult = result; + bool threw = false; + try { + DeviceBuffer failed(context, bytes, usage); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("Vulkan result " + std::to_string(result)) != std::string::npos; + } + const auto retained = mock.liveBytes; + Expect(threw && mock.binds == 1, "a failed device buffer bind lost its error"); + Expect(retained == 0 && mock.liveBuffers.empty(), "a failed device buffer was retained for reuse"); + mock.bindResult = VK_SUCCESS; + unsigned invalid = 0; + for (unsigned retry = 0; retry < 3; ++retry) { + DeviceBuffer recovered(context, bytes, usage); + if (!mock.bound.contains(recovered.Handle())) ++invalid; + } + Expect(invalid == 0, "a retry reused an unbound device buffer"); + Expect(mock.binds == 2 && mock.allocations == 2, "successfully bound device buffers stopped being reused"); + std::printf("device bind result=%d retained=%llu invalid retries=%u binds=%u\n", static_cast(result), static_cast(retained), invalid, mock.binds); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "device bind recovery leaked a resource"); + } } -int main() { +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { + SmallDeviceRetention(); + return failures == 0 ? 0 : 1; + } SizeClassesAndDirections(); LargerClass(); KeptUntilFence(); + SmallDeviceRetention(); Budget(); AddressAndHostUnchanged(); SmallDeviceClasses(); + SlabMetadataFailure(); + SlabMapFailure(); + DeviceBindingFailures(); Slabs(); for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) { AllocationRecovery(result, false); @@ -551,6 +787,7 @@ int main() { AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); SuccessfulAllocationKeepsPool(); PartialSlabDoesNotRetry(); + CacheReclaimRecovery(); HostPlacement(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp b/core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp new file mode 100644 index 0000000000..65b5f0e36a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp @@ -0,0 +1,20 @@ +#include "CommandBatchFailure.hpp" +#include "AllocationFailure.hpp" +#include +#include +#include + +int main() { + try { + AgcDriver::Graphics::Context context{}; + context.device = reinterpret_cast(std::uintptr_t{1}); + context.queue = reinterpret_cast(std::uintptr_t{2}); + context.pool = reinterpret_cast(std::uintptr_t{3}); + AgcDriver::Tests::RunCommandBatchFailureTests(context, false); + return 0; + } catch (const std::exception& error) { + AgcDriver::Tests::SetAllocationFailureCountdown(-1); + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp new file mode 100644 index 0000000000..71385b5298 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp @@ -0,0 +1,206 @@ +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "AllocationFailure.hpp" +#include "CommandBatchFailure.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::Tests::SetAllocationFailureCountdown; + +enum class Fault { None, Timing, End, Submit }; + +struct Probe { + PFN_vkGetDeviceProcAddr real = nullptr; + Fault fault = Fault::None; + VkFence fence = VK_NULL_HANDLE; + bool pending = false; + unsigned submitted = 0; + unsigned queries = 0; + unsigned idleWaits = 0; + unsigned fenceWaits = 0; + unsigned premature = 0; + unsigned safetyWaits = 0; + unsigned nativePending = 0; + unsigned freed = 0; + unsigned destroyed = 0; +}; + +Probe probe; +VkDevice activeDevice = VK_NULL_HANDLE; +VkQueue activeQueue = VK_NULL_HANDLE; + +void GuardRelease(VkDevice device, VkQueue queue) { + if (!probe.pending) return; + ++probe.premature; + ++probe.safetyWaits; + if (probe.real != nullptr) { + const auto status = reinterpret_cast(probe.real(device, "vkGetFenceStatus"))(device, probe.fence); + if (status == VK_NOT_READY) ++probe.nativePending; + else if (status != VK_SUCCESS) std::terminate(); + const auto wait = reinterpret_cast(probe.real(device, "vkQueueWaitIdle")); + if (wait(queue) != VK_SUCCESS) std::terminate(); + } + probe.pending = false; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateCommands(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { + *commands = reinterpret_cast(std::uintptr_t{4}); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { + *fence = reinterpret_cast(std::uintptr_t{5}); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL BeginCommands(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; } + +VKAPI_ATTR VkResult VKAPI_CALL EndCommands(VkCommandBuffer commands) { + if (probe.fault == Fault::End) return VK_ERROR_OUT_OF_HOST_MEMORY; + if (probe.real == nullptr) return VK_SUCCESS; + return reinterpret_cast(probe.real(activeDevice, "vkEndCommandBuffer"))(commands); +} + +VKAPI_ATTR VkResult VKAPI_CALL Submit(VkQueue queue, std::uint32_t count, const VkSubmitInfo* submissions, VkFence fence) { + if (probe.fault == Fault::Submit) return VK_ERROR_OUT_OF_HOST_MEMORY; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(activeDevice, "vkQueueSubmit"))(queue, count, submissions, fence) : VK_SUCCESS; + if (result == VK_SUCCESS) { + ++probe.submitted; + probe.pending = true; + probe.fence = fence; + if (probe.fault == Fault::Timing) SetAllocationFailureCountdown(0); + } + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL FenceStatus(VkDevice device, VkFence fence) { + ++probe.queries; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(device, "vkGetFenceStatus"))(device, fence) : VK_NOT_READY; + if (result == VK_SUCCESS) probe.pending = false; + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL QueueIdle(VkQueue queue) { + ++probe.idleWaits; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(activeDevice, "vkQueueWaitIdle"))(queue) : VK_SUCCESS; + if (result == VK_SUCCESS) probe.pending = false; + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL WaitFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) { + ++probe.fenceWaits; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(device, "vkWaitForFences"))(device, count, fences, all, timeout) : VK_SUCCESS; + if (result == VK_SUCCESS) probe.pending = false; + return result; +} + +VKAPI_ATTR void VKAPI_CALL FreeCommands(VkDevice device, VkCommandPool pool, std::uint32_t count, const VkCommandBuffer* commands) { + GuardRelease(device, activeQueue); + probe.freed += count; + if (probe.real != nullptr) reinterpret_cast(probe.real(device, "vkFreeCommandBuffers"))(device, pool, count, commands); +} + +VKAPI_ATTR void VKAPI_CALL DestroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) { + GuardRelease(device, activeQueue); + ++probe.destroyed; + if (probe.real != nullptr) reinterpret_cast(probe.real(device, "vkDestroyFence"))(device, fence, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkEndCommandBuffer") == 0) return reinterpret_cast(EndCommands); + if (std::strcmp(name, "vkQueueSubmit") == 0) return reinterpret_cast(Submit); + if (std::strcmp(name, "vkGetFenceStatus") == 0) return reinterpret_cast(FenceStatus); + if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast(QueueIdle); + if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast(WaitFences); + if (std::strcmp(name, "vkFreeCommandBuffers") == 0) return reinterpret_cast(FreeCommands); + if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast(DestroyFence); + if (probe.real != nullptr) return probe.real(device, name); + if (std::strcmp(name, "vkAllocateCommandBuffers") == 0) return reinterpret_cast(AllocateCommands); + if (std::strcmp(name, "vkCreateFence") == 0) return reinterpret_cast(CreateFence); + if (std::strcmp(name, "vkBeginCommandBuffer") == 0) return reinterpret_cast(BeginCommands); + return nullptr; +} + +bool RunCase(const Context& original, Fault fault, bool wait, bool native) { + probe = {}; + probe.real = native ? original.deviceProc : nullptr; + probe.fault = fault; + activeDevice = original.device; + activeQueue = original.queue; + auto context = original; + context.deviceProc = DeviceProc; + std::unique_ptr output; + constexpr std::size_t bytes = 256; + constexpr std::uint32_t pattern = 0x5a3c71e9u; + if (native) { + output = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(output->Bytes().data(), 0, bytes); + } + bool allocationFailed = false; + bool apiFailed = false; + bool waitFailed = false; + { + AgcDriver::FrameTiming frame(1, true); + AgcDriver::PerformanceContext timing(&frame); + try { + CommandBatch batch(context); + if (output) { + context.Function("vkCmdFillBuffer")(batch.Handle(), output->Handle(), 0, bytes, pattern); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + } + try { + batch.Submit(); + } catch (const std::bad_alloc&) { + if (!wait) throw; + allocationFailed = true; + } + SetAllocationFailureCountdown(-1); + if (wait && probe.submitted != 0) { + try { + batch.Wait(); + } catch (const std::exception&) { + waitFailed = true; + } + } + } catch (const std::bad_alloc&) { + allocationFailed = true; + } catch (const std::runtime_error& error) { + apiFailed = std::string(error.what()).find(fault == Fault::End ? "vkEndCommandBuffer" : "vkQueueSubmit graphics") != std::string::npos; + } + SetAllocationFailureCountdown(-1); + } + bool pixels = true; + if (output && probe.submitted != 0) { + output->Invalidate(); + const auto* words = reinterpret_cast(output->Bytes().data()); + pixels = std::all_of(words, words + bytes / sizeof(*words), [](auto word) { return word == pattern; }); + } + const bool rejected = fault == Fault::End || fault == Fault::Submit; + const bool complete = rejected ? probe.queries == 0 && probe.idleWaits == 0 && probe.fenceWaits == 0 : wait ? probe.fenceWaits == 1 : probe.queries == 1; + const bool passed = allocationFailed == (fault == Fault::Timing) && apiFailed == rejected && !waitFailed && !probe.pending && probe.submitted == (rejected ? 0u : 1u) && probe.premature == 0 && probe.freed == 1 && probe.destroyed == 1 && complete && pixels; + std::printf("command batch fault=%u wait=%u allocation failed=%u submitted=%u premature=%u safety waits=%u queries=%u idle waits=%u fence waits=%u readbacks=%u correct=%u wait failed=%u native pending=%u\n", static_cast(fault), wait, allocationFailed, probe.submitted, probe.premature, probe.safetyWaits, probe.queries, probe.idleWaits, probe.fenceWaits, output && probe.submitted != 0 ? 1u : 0u, pixels, waitFailed, probe.nativePending); + return passed; +} + +} + +void AgcDriver::Tests::RunCommandBatchFailureTests(const AgcDriver::Graphics::Context& context, bool native) { + unsigned failures = 0; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const auto fault : {Fault::Timing, Fault::None}) { + for (const auto wait : {false, true}) if (!RunCase(context, fault, wait, native)) ++failures; + } + for (const auto fault : {Fault::End, Fault::Submit}) if (!RunCase(context, fault, false, native)) ++failures; + Require(failures == 0, "command batch failure handling released pending work or lost its submission state"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp new file mode 100644 index 0000000000..ed90a19297 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_COMMANDBATCHFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_COMMANDBATCHFAILURE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Tests { + +void RunCommandBatchFailureTests(const Graphics::Context& context, bool native); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp index 5324f2e432..51d13f59c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -62,6 +62,10 @@ void CheckRoundTrip() { changed = decoded; changed.requiredSubgroupSize = 64; Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity"); + changed = decoded; + changed.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; + Require(AgcDriver::ComputePipelineCapture::Decode(changed.Encode()).stageFlags == changed.stageFlags, "full subgroups were lost in the capture"); + Require(changed.Identity() != capture.Identity(), "full subgroups did not affect capture identity"); Directory directory; const auto file = capture.Write(directory.path); Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs"); @@ -144,7 +148,11 @@ void CheckMalformedRecords() { invalid([](auto& copy) { copy.spirv[11] = 4; }); invalid([](auto& copy) { copy.spirv[13] = 0; }); invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; }); - invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; }); + invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_ALLOW_VARYING_SUBGROUP_SIZE_BIT; }); + invalid([](auto& copy) { + copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; + copy.requiredSubgroupSize = 0; + }); invalid([](auto& copy) { copy.requiredSubgroupSize = 3; }); invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); }); invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index 38131c019d..d660dc354d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -1,5 +1,6 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" #include #include #include @@ -44,6 +45,48 @@ bool reversesTo(std::uint32_t word, std::uint32_t expected) { return result->HasImmediate() && result->ImmediateBool(); } +bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32_t version, bool expected, bool directLane = false, IrShaderStage stage = IrShaderStage::Vertex, bool ballotWords = false) { + IrProgram program; + program.Resources().stage = stage; + program.SetWaveSize(64u); + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& half = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(32u)}); + IrValue* lowWord = &builder.Constant(low); + IrValue* highWord = &builder.Constant(0u); + if (ballotWords) { + auto& entry = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&builder.ConstantBool(true)}); + lowWord = &builder.Emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&entry, &builder.Constant(0u)}); + highWord = &builder.Emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&entry, &builder.Constant(1u)}); + } + auto& word = builder.Emit(IrOpcode::SelectU32, IrType::U32, {&half, lowWord, highWord}); + auto& amount = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&lane, &builder.Constant(31u)}); + auto& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &amount}); + auto& bit = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&shifted, &builder.Constant(1u)}); + auto& test = builder.Emit(IrOpcode::INotEqual32, IrType::Bool, {&bit, &builder.Constant(0u)}); + auto& user = builder.Emit(IrOpcode::Reference, IrType::Void, {&test}); + if (directLane) static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&lane})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder folder; + const bool folded = folder.FoldVertexLaneBounds(program, hostSubgroup, version); + const bool shouldFold = !ballotWords && stage == IrShaderStage::Vertex && hostSubgroup == 32u && version >= 0x00010000u && version < 0x00010600u; + if (folded != shouldFold) return false; + if (!shouldFold && (word.Opcode() != IrOpcode::SelectU32 || word.Argument(0)->Resolve() != &half || half.Opcode() != IrOpcode::ULessThan32)) return false; + if (shouldFold && (!word.Resolve()->HasImmediate() || word.Resolve()->ImmediateU32() != low)) return false; + folder.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + const auto* result = user.Argument(0)->Resolve(); + if (expected) return result->HasImmediate() && result->ImmediateBool(); + bool retainedLane = false; + for (const auto* instruction : block.Instructions()) retainedLane = retainedLane || instruction->Opcode() == IrOpcode::LaneId; + return retainedLane; +} + bool run(const char* name, bool passed) { if (!passed) std::fprintf(stderr, "%s\n", name); return passed; @@ -58,6 +101,18 @@ int main() { passed &= run("an all-one word must fold the lane bit test to true", foldsTo(0xffffffffu, true, true)); passed &= run("a mixed word must keep the lane bit test", foldsTo(0x0000ffffu, false, true)); passed &= run("an unmasked lane number must keep the lane bit test", foldsTo(0xffffffffu, true, false)); + passed &= run("a fixed 32-lane vertex subgroup removes a constant lower-half export guard", vertexProjection(0xffffffffu, 32u, 0x00010300u, true)); + passed &= run("a single set bit remains dependent on the vertex lane", vertexProjection(1u, 32u, 0x00010300u, false)); + passed &= run("a partial mask remains dependent on the vertex lane", vertexProjection(0x0000ffffu, 32u, 0x00010300u, false)); + passed &= run("a real vertex lane output stays observable after folding its guard", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, true)); + passed &= run("a lane word selected from ballot words keeps its lower-half test", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, false, IrShaderStage::Vertex, true)); + passed &= run("a 64-lane subgroup keeps a lower-half export guard", vertexProjection(0xffffffffu, 64u, 0x00010300u, false)); + passed &= run("an unknown subgroup keeps a lower-half export guard", vertexProjection(0xffffffffu, 0u, 0x00010300u, false)); + passed &= run("a single-lane request does not borrow the physical 32-lane proof", vertexProjection(0xffffffffu, 1u, 0x00010300u, false)); + passed &= run("SPIR-V 1.6 does not borrow a fixed subgroup-size proof", vertexProjection(0xffffffffu, 32u, 0x00010600u, false)); + passed &= run("an unknown module version keeps the lane guard", vertexProjection(0xffffffffu, 32u, 0u, false)); + passed &= run("the lane-bound optimization does not change compute shaders", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, false, IrShaderStage::Compute)); + passed &= run("the lane-bound optimization does not change pixel shaders", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, false, IrShaderStage::Pixel)); passed &= run("a reversed zero must fold to zero", reversesTo(0x00000000u, 0x00000000u)); passed &= run("a reversed single bit must fold to the opposite end", reversesTo(0x00000001u, 0x80000000u)); passed &= run("a reversed high bit must fold to bit zero", reversesTo(0x80000000u, 0x00000001u)); diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index a427dfcc29..ebb60efe01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -10,12 +10,14 @@ #include #include #include +#include #include namespace { using namespace AgcDriver::Graphics; +constexpr std::uint32_t FormatR8Unorm = 1; constexpr std::uint32_t FormatR8Uint = 5; constexpr std::uint32_t FormatR16Unorm = 7; constexpr std::uint32_t FormatR16Uint = 11; @@ -43,6 +45,8 @@ VkImage lastImage = VK_NULL_HANDLE; std::vector clearedImages; VkImageUsageFlags lastImageUsage = 0; std::uint32_t imagesDestroyed = 0; +std::vector> imageReads; +std::vector> imageWrites; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; @@ -114,6 +118,33 @@ VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImage for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); } +VKAPI_ATTR void VKAPI_CALL cmdCopyImageToBuffer(VkCommandBuffer, VkImage source, VkImageLayout, VkBuffer, std::uint32_t count, const VkBufferImageCopy* regions) { + for (std::uint32_t i = 0; i < count; ++i) imageReads.emplace_back(source, regions[i].imageSubresource.aspectMask); +} + +VKAPI_ATTR void VKAPI_CALL cmdCopyBufferToImage(VkCommandBuffer, VkBuffer, VkImage destination, VkImageLayout, std::uint32_t count, const VkBufferImageCopy* regions) { + for (std::uint32_t i = 0; i < count; ++i) imageWrites.emplace_back(destination, regions[i].imageSubresource.aspectMask); +} + +VKAPI_ATTR VkResult VKAPI_CALL createBuffer(VkDevice, const VkBufferCreateInfo*, const VkAllocationCallbacks*, VkBuffer* buffer) { + *buffer = newHandle(); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL bufferMemoryRequirements(VkDevice, VkBuffer, VkMemoryRequirements* requirements) { + *requirements = {65536, 256, 1}; +} + +VKAPI_ATTR VkResult VKAPI_CALL bindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL destroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) {} +VKAPI_ATTR VkResult VKAPI_CALL mapMemory(VkDevice, VkDeviceMemory, VkDeviceSize, VkDeviceSize, VkMemoryMapFlags, void**) { + return VK_ERROR_MEMORY_MAP_FAILED; +} +VKAPI_ATTR void VKAPI_CALL unmapMemory(VkDevice, VkDeviceMemory) {} + VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { *properties = {}; properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; @@ -163,7 +194,15 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { {"vkFreeMemory", reinterpret_cast(freeMemory)}, {"vkCmdPipelineBarrier", reinterpret_cast(cmdPipelineBarrier)}, {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)}, - {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)} + {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)}, + {"vkCmdCopyImageToBuffer", reinterpret_cast(cmdCopyImageToBuffer)}, + {"vkCmdCopyBufferToImage", reinterpret_cast(cmdCopyBufferToImage)}, + {"vkCreateBuffer", reinterpret_cast(createBuffer)}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(bufferMemoryRequirements)}, + {"vkBindBufferMemory", reinterpret_cast(bindBufferMemory)}, + {"vkDestroyBuffer", reinterpret_cast(destroyBuffer)}, + {"vkMapMemory", reinterpret_cast(mapMemory)}, + {"vkUnmapMemory", reinterpret_cast(unmapMemory)} }; const auto it = table.find(name); return it == table.end() ? nullptr : it->second; @@ -177,6 +216,9 @@ struct View { std::uint32_t tileMode = TileDepth64KB; std::uint32_t type = Type2D; std::uint32_t lastArray = 0; + std::uint32_t baseLevel = 0; + std::uint32_t lastLevel = 0; + std::uint32_t maxMip = 0; }; std::array tsharp(const View& view) { @@ -187,8 +229,9 @@ std::array tsharp(const View& view) { words[0] = static_cast(base); words[1] = static_cast((base >> 32u) & 0xffu) | ((view.format & 0x1ffu) << 20u) | ((widthMinus1 & 0x3u) << 30u); words[2] = ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u); - words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | ((view.tileMode & 0x1fu) << 20u) | ((view.type & 0xfu) << 28u); + words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | ((view.baseLevel & 0xfu) << 12u) | ((view.lastLevel & 0xfu) << 16u) | ((view.tileMode & 0x1fu) << 20u) | ((view.type & 0xfu) << 28u); words[4] = view.lastArray & 0x1fffu; + words[5] = (view.maxMip & 0xfu) << 4u; return words; } @@ -197,6 +240,37 @@ std::shared_ptr lookup(const Context& context, const View& view) { return DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); } +void TestStencilUnormView(const Context& context) { + constexpr std::uint64_t depth = 0x74000000; + constexpr std::uint64_t stencil = 0x74800000; + constexpr std::uint64_t other = 0x75000000; + const auto before = createdImages.size(); + DepthSurfaceView(context, {depth, stencil, {320, 180}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + Require(createdImages.size() == before + 1, "binding a new depth surface must create one image"); + const auto surface = createdImages[before]; + DepthSurfaceView(context, {other, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + imageReads.clear(); + imageWrites.clear(); + Require(lookup(context, {stencil, FormatR8Uint, 320, 180}) != nullptr && imageReads.empty() && imageWrites.empty(), "an R8_UINT view of a stencil plane must sample it without a copy"); + const auto first = lookup(context, {stencil, FormatR8Unorm, 320, 180}); + Require(first != nullptr && lastImageFormat == VK_FORMAT_R8_UNORM, "an R8_UNORM view of a stencil plane must sample an R8_UNORM copy of it"); + const auto copy = createdImages.back(); + const std::vector> read{{surface, VK_IMAGE_ASPECT_STENCIL_BIT}}; + const std::vector> written{{copy, VK_IMAGE_ASPECT_COLOR_BIT}}; + Require(imageReads == read && imageWrites == written, "the R8_UNORM copy must be filled from the stencil aspect of the depth image"); + Require(lookup(context, {stencil, FormatR8Unorm, 320, 180}) == first && imageReads.size() == 1, "sampling the stencil plane again with no write in between must reuse the copy"); + DepthSurfaceView(context, {depth, stencil, {320, 180}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + Require(imageReads.size() == 3 && imageWrites.size() == 3, "sampling the stencil plane of the bound depth surface must copy it each time"); + DepthSurfaceView(context, {other, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + Require(imageReads.size() == 4 && imageWrites.size() == 4, "after the surface is unbound, its stencil copy must be refreshed once"); + Require(createdImages.back() == copy, "refreshing the stencil copy must reuse its image"); + ClearDepthSurfaces(context.device); +} + 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; @@ -314,6 +388,7 @@ void RunDepthSurfaceReuseTests() { TestD16StencilFallback(context); TestMultisampleDepthOwnership(context); TestMultisampleStencilAliases(context); + TestStencilUnormView(context); constexpr std::uint64_t depth32 = 0x40000000; constexpr std::uint64_t depth16 = 0x50000000; constexpr std::uint64_t depthStencil = 0x60000000; @@ -332,6 +407,8 @@ void RunDepthSurfaceReuseTests() { Require(lookup(context, {depth32, FormatR32Float, 512, 512, TileDepth64KB, Type2DArray, 5}) == nullptr, "a 2D array view of another extent over a depth surface must be read as reused memory"); Require(lookup(context, {depth32, FormatR32Uint, 384, 384, TileRenderTarget64KB}) == nullptr, "an R32 uint view without a depth layout must be read as reused memory"); Require(lookup(context, {depth16, FormatR32Float, 256, 256}) == nullptr, "an R32F view of a D16 surface's own extent must be read as reused memory"); + Require(lookup(context, {depth32, FormatR32Float, 384, 384, TileDepth64KB, Type2D, 0, 0, 0, 4}) != nullptr, "the first level of a mipmapped R32F view of a D32 surface's own extent must sample its depth plane"); + Require(lookup(context, {depth32, FormatR32Float, 384, 384, TileDepth64KB, Type2D, 0, 1, 1, 4}) == nullptr, "a view that starts past the first level of a D32 surface's own extent must be read as reused memory"); Require(lookup(context, {stencil, FormatR8Uint, 320, 180}) != nullptr, "an 8-bit view of a stencil plane's own extent must sample the stencil plane"); Require(lookup(context, {stencil, FormatR8G8B8A8Unorm, 320, 180, TileRenderTarget64KB}) == nullptr, "an RGBA8 view of a stencil plane's own extent must be read as reused memory"); diff --git a/core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp new file mode 100644 index 0000000000..070d6ab58e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp @@ -0,0 +1,169 @@ +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { +thread_local bool failNextAllocation = false; +} + +void* operator new(std::size_t size) { + if (failNextAllocation) { + failNextAllocation = false; + throw std::bad_alloc(); + } + if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} + +void* operator new[](std::size_t size) { return ::operator new(size); } +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete[](void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } +void operator delete[](void* memory, std::size_t) noexcept { std::free(memory); } + +namespace { +using namespace AgcDriver::Graphics; + +struct VulkanObjects { + std::uintptr_t next = 1; + std::uint32_t layouts = 0; + std::uint32_t pools = 0; + std::uint32_t createdLayouts = 0; + std::uint32_t createdPools = 0; + bool failAfterLayout = false; + bool failAfterPool = false; + std::array poolUsed{}; +} objects; + +void RequireTest(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo*, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) { + *layout = reinterpret_cast(objects.next++); + ++objects.layouts; + ++objects.createdLayouts; + failNextAllocation = objects.failAfterLayout; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) { + RequireTest(objects.layouts != 0, "layout destroyed twice"); + --objects.layouts; +} + +VKAPI_ATTR VkResult VKAPI_CALL CreatePool(VkDevice, const VkDescriptorPoolCreateInfo*, const VkAllocationCallbacks*, VkDescriptorPool* pool) { + RequireTest(objects.next < objects.poolUsed.size(), "mock pool capacity exceeded"); + *pool = reinterpret_cast(objects.next++); + ++objects.pools; + ++objects.createdPools; + failNextAllocation = objects.failAfterPool; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyPool(VkDevice, VkDescriptorPool, const VkAllocationCallbacks*) { + RequireTest(objects.pools != 0, "pool destroyed twice"); + --objects.pools; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateSets(VkDevice, const VkDescriptorSetAllocateInfo* info, VkDescriptorSet* set) { + auto& used = objects.poolUsed.at(reinterpret_cast(info->descriptorPool)); + if (used) return VK_ERROR_OUT_OF_POOL_MEMORY; + used = true; + *set = reinterpret_cast(objects.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL ResetPool(VkDevice, VkDescriptorPool pool, VkDescriptorPoolResetFlags) { + objects.poolUsed.at(reinterpret_cast(pool)) = false; + return VK_SUCCESS; +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice, const char* name) { + if (std::strcmp(name, "vkCreateDescriptorSetLayout") == 0) return reinterpret_cast(CreateLayout); + if (std::strcmp(name, "vkDestroyDescriptorSetLayout") == 0) return reinterpret_cast(DestroyLayout); + if (std::strcmp(name, "vkCreateDescriptorPool") == 0) return reinterpret_cast(CreatePool); + if (std::strcmp(name, "vkDestroyDescriptorPool") == 0) return reinterpret_cast(DestroyPool); + if (std::strcmp(name, "vkAllocateDescriptorSets") == 0) return reinterpret_cast(AllocateSets); + if (std::strcmp(name, "vkResetDescriptorPool") == 0) return reinterpret_cast(ResetPool); + return nullptr; +} + +Context TestContext() { + Context context{}; + context.deviceProc = Resolve; + context.limits.maxPerStageResources = 128; + context.limits.maxDescriptorSetStorageBuffers = 4096; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 4096; + return context; +} + +const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; +const std::array key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags}; +const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}; + +bool LayoutFailure(bool updateAfterBind) { + objects = {}; + bool failed = false; + { + DescriptorCache cache(TestContext()); + objects.failAfterLayout = true; + try { + cache.Layout(key, std::span(&binding, 1), updateAfterBind); + } catch (const std::bad_alloc&) { + failed = true; + } + failNextAllocation = false; + objects.failAfterLayout = false; + RequireTest(failed && objects.createdLayouts == 1, "layout publication failure was not exercised"); + const auto layout = cache.Layout(key, std::span(&binding, 1), updateAfterBind); + RequireTest(layout != VK_NULL_HANDLE && objects.createdLayouts == 2, "layout creation did not recover"); + RequireTest(cache.Layout(key, std::span(&binding, 1), updateAfterBind) == layout && objects.createdLayouts == 2, "layout cache hit created another object"); + } + std::printf("layout updateAfterBind=%d: unowned=%u\n", updateAfterBind, objects.layouts); + return objects.layouts == 0; +} + +bool PoolFailure(bool updateAfterBind, bool grow) { + objects = {}; + { + DescriptorCache cache(TestContext()); + const auto layout = cache.Layout(key, std::span(&binding, 1), updateAfterBind); + DescriptorCache::SetAllocation first{}; + if (grow) first = cache.Allocate(layout, std::span(&size, 1), updateAfterBind); + objects.failAfterPool = true; + DescriptorCache::SetAllocation allocation{}; + try { + allocation = cache.Allocate(layout, std::span(&size, 1), updateAfterBind); + } catch (const std::bad_alloc&) { + } + failNextAllocation = false; + objects.failAfterPool = false; + RequireTest(objects.createdPools == (grow ? 2u : 1u), "pool publication path was not exercised"); + if (allocation.set != VK_NULL_HANDLE) cache.Free(allocation); + if (grow) cache.Free(first); + const auto recovered = cache.Allocate(layout, std::span(&size, 1), updateAfterBind); + RequireTest(recovered.set != VK_NULL_HANDLE, "pool allocation did not recover"); + cache.Free(recovered); + } + std::printf("pool updateAfterBind=%d grow=%d: unowned=%u\n", updateAfterBind, grow, objects.pools); + return objects.layouts == 0 && objects.pools == 0; +} +} + +int main() { + int failures = 0; + for (const bool updateAfterBind : {false, true}) { + if (!LayoutFailure(updateAfterBind)) ++failures; + for (const bool grow : {false, true}) if (!PoolFailure(updateAfterBind, grow)) ++failures; + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp new file mode 100644 index 0000000000..ec52e5f1d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +void Close(int descriptor) { +#ifdef _WIN32 + _close(descriptor); +#else + close(descriptor); +#endif +} + +int Read(int descriptor, void* bytes, unsigned count) { +#ifdef _WIN32 + return _read(descriptor, bytes, count); +#else + return static_cast(read(descriptor, bytes, count)); +#endif +} + +std::FILE* Pipe(int& reader) { + int descriptors[2]; +#ifdef _WIN32 + Check(_pipe(descriptors, 4096, _O_BINARY) == 0, "cannot create test pipe"); + auto* file = _fdopen(descriptors[1], "wb"); +#else + Check(pipe(descriptors) == 0, "cannot create test pipe"); + auto* file = fdopen(descriptors[1], "wb"); +#endif + Check(file != nullptr, "cannot open test pipe writer"); + reader = descriptors[0]; + return file; +} + +void SuccessfulBytes() { + auto* file = std::tmpfile(); + Check(file != nullptr, "cannot create BMP test file"); + const std::array pixels{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + Check(!AgcDriver::WriteBmpStream_nid_no_patch(file, 2, 2, pixels), "valid BMP write failed"); + Check(std::fflush(file) == 0, "valid BMP flush failed"); + std::rewind(file); + std::array actual{}; + Check(std::fread(actual.data(), 1, actual.size(), file) == actual.size(), "BMP has the wrong byte count"); + const std::array header{ + 0x42, 0x4d, 70, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, + 40, 0, 0, 0, 2, 0, 0, 0, 0xfe, 0xff, 0xff, 0xff, + 1, 0, 32, 0, 0, 0, 0, 0, 16, 0, 0, 0, + 0x13, 0x0b, 0, 0, 0x13, 0x0b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + Check(std::memcmp(actual.data(), header.data(), header.size()) == 0, "BMP header changed"); + Check(std::memcmp(actual.data() + header.size(), pixels.data(), pixels.size()) == 0, "BMP pixel payload changed"); + Check(std::fclose(file) == 0, "BMP test close failed"); +} + +void HeaderFailure() { + int reader; + auto* file = Pipe(reader); + Check(std::setvbuf(file, nullptr, _IONBF, 0) == 0, "cannot disable pipe buffering"); + Close(reader); + const std::array pixels{}; + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 2, 2, pixels); + Check(failure && std::strcmp(failure.operation, "header") == 0, "closed pipe header write was accepted"); +} + +void PixelFailure() { + int reader; + auto* file = Pipe(reader); + Check(std::setvbuf(file, nullptr, _IONBF, 0) == 0, "cannot disable pipe buffering"); + unsigned received = 0; + std::thread consumer([&] { + std::array header{}; + while (received < header.size()) { + const auto count = Read(reader, header.data() + received, static_cast(header.size() - received)); + if (count <= 0) break; + received += static_cast(count); + } + Close(reader); + }); + const std::vector pixels(512 * 512 * 4); + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 512, 512, pixels); + consumer.join(); + Check(received == 54, "pixel failure did not follow a complete BMP header"); + Check(failure && std::strcmp(failure.operation, "pixels") == 0, "closed pipe pixel write was accepted"); +} + +void CloseFailure() { + int reader; + auto* file = Pipe(reader); + std::array buffer{}; + Check(std::setvbuf(file, buffer.data(), _IOFBF, buffer.size()) == 0, "cannot buffer test pipe"); + Close(reader); + const std::array pixels{}; + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 2, 2, pixels); + Check(failure && std::strcmp(failure.operation, "close") == 0, "failed buffered close was accepted"); +} + +void DisableAfterFailure() { + const auto directory = std::filesystem::temp_directory_path() / ("anyps5-bmp-io-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Check(std::filesystem::create_directory(directory), "cannot create output-failure fixture"); + AgcDriver::FrameDumpOutput output; + const std::array pixels{}; + Check(output.Enabled_nid_no_patch(), "BMP output was disabled before any failure"); + const auto blocked = directory.string(); + Check(!output.Write_nid_no_patch(blocked.c_str(), 2, 2, pixels), "BMP output accepted a directory as a file"); + Check(!output.Enabled_nid_no_patch(), "failed BMP output remained enabled"); + const auto skipped = directory / "must-not-exist.bmp"; + const auto path = skipped.string(); + for (unsigned i = 0; i < 64; ++i) Check(!output.Write_nid_no_patch(path.c_str(), 2, 2, pixels), "disabled BMP output wrote another file"); + Check(!std::filesystem::exists(skipped), "disabled BMP output created a file"); + Check(std::filesystem::remove(directory), "cannot remove empty fixture directory"); +} + +} + +int main() { +#ifndef _WIN32 + const auto previous = std::signal(SIGPIPE, SIG_IGN); +#endif + SuccessfulBytes(); + HeaderFailure(); + PixelFailure(); + CloseFailure(); + DisableAfterFailure(); +#ifndef _WIN32 + std::signal(SIGPIPE, previous); +#endif + std::puts("frame dump I/O tests passed: bytes/header/pixels/close/disable"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e247500bd3..3c750efbbb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" @@ -25,15 +26,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "ImageTableAbi.hpp" +#include "SpirvBackend/SpirvSpecialization.hpp" #include #include #include +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -598,6 +605,15 @@ void hardwareScreenOffsetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void depthCopyFormatTests() { + using AgcDriver::Graphics::Texture; + Require(Texture::DepthCopyFormat(VK_FORMAT_R32_SFLOAT) == VK_FORMAT_D32_SFLOAT, "a 32-bit float color format did not map to the 32-bit float depth format"); + Require(Texture::DepthCopyFormat(VK_FORMAT_R16_UNORM) == VK_FORMAT_D16_UNORM, "a 16-bit unorm color format did not map to the 16-bit unorm depth format"); + for (const auto format : {VK_FORMAT_UNDEFINED, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_D32_SFLOAT}) { + Require(Texture::DepthCopyFormat(format) == VK_FORMAT_UNDEFINED, "a color format without a depth counterpart mapped to a depth format"); + } +} + void UnwrittenSampleLocationTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; @@ -816,6 +832,10 @@ void ShaderStageTests() { Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry"); const auto& mesh = *state.stages.mesh; Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR"); + fast.context[0x29b] = 1; + const auto fastLines = AgcDriver::Graphics::DecodeState(fast); + Require(fastLines.stages.mesh->outputPrimitiveSize == 2 && fastLines.stages.mesh->maxPrimitives == 64 && AgcDriver::Graphics::RasterPrimitiveOf(fastLines) == ShaderRecompiler::RasterPrimitive::Line, "a fast-launch line strip did not take its line count from PRIM_AMP_FACTOR"); + fast.context[0x29b] = 2; fast.context[0x2d5] = 0x502030; expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state"); fast.context[0x2d5] = 0x482030; @@ -828,6 +848,40 @@ void ShaderStageTests() { fast.context[0x2d3] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives"); } + { + auto lines = makeState(); + lines.userConfig[0x242] = 1; + lines.context[0x2d5] = 0x2030; + lines.userConfig[0x25b] = 0x6633; + lines.context[0x1ff] = 255; + lines.context[0x2ce] = 5; + lines.context[0x29b] = 1; + lines.context[0x2ab] = 4; + lines.shader[0x8a] = 3u << 29u; + lines.shader[0x8b] = 3u << 16u; + auto state = AgcDriver::Graphics::DecodeState(lines); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "a geometry program that outputs line strips did not decode as geometry rasterized as lines"); + auto mesh = *state.stages.mesh; + Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 51 && mesh.verticesPerGroup == 51 && mesh.maxVertices == 255 && mesh.maxPrimitives == 255 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 2, "a line-strip subgroup did not decode as 51 points with a line slot per output vertex"); + lines.context[0x29b] = 2; + state = AgcDriver::Graphics::DecodeState(lines); + mesh = *state.stages.mesh; + Require(mesh.maxPrimitives == 153 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 3 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "the triangle-strip subgroup of the same draw changed"); + lines.context[0x29b] = 1; + lines.context[0x2ce] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly"); + lines.context[0x2ce] = 5; + for (const auto output : {0u, 3u, 0x80000001u, 0x101u}) { + lines.context[0x29b] = output; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); + } + lines.userConfig[0x242] = 4; + lines.context[0x205] = 0x2a40; + lines.context[0x29b] = 2; + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->primitivesPerGroup == 17, "triangle input with a triangle-strip output and a triangle polygon offset did not decode"); + lines.context[0x29b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "polygon mode, depth bias or nonstandard rasterization"); + } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; @@ -947,6 +1001,9 @@ void NggVertexWorkgroupTests() { queue.context[0x2d5] = stages; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); } + queue.context[0x2d5] = 0x12010; + queue.context[0x29b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); } constexpr std::array IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}; @@ -1282,6 +1339,48 @@ void BlendBypassTests() { Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends"); } +void UnclampedNormalizedColorTests() { + using AgcDriver::Graphics::DecodeState; + constexpr std::uint32_t blending = (1u << 30u) | 4u | (5u << 8u); + const auto target = [](std::uint32_t info) { + auto queue = makeState(); + for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; + queue.context[0x31c] = info; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + auto queue = target(0x0000000cu); + auto state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8_UNORM && !state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an 8_8 UNORM target with BLEND_CLAMP clear (CB_COLOR_INFO 0x0000000c, PPSA26344) was refused without blending"); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused an unclamped UNORM target that does not blend: " + rejection); + Require(AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0).format == VK_FORMAT_R8G8_UNORM, "an unclamped UNORM target was refused outside a draw"); + auto eliminate = target(0x0000000cu); + eliminate.context[0x0] = 0; + eliminate.context[0x202] = 0xcc0020; + const auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(eliminate); + Require(pass.has_value() && pass->mode == AgcDriver::Graphics::ColorMetadataPass::Mode::EliminateFastClear && pass->targets.size() == 1 && pass->targets[0].format == VK_FORMAT_R8G8_UNORM, "a fast-clear eliminate over an unclamped UNORM target was refused"); + queue.context[0x1e0] = blending; + expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504)"); + queue.context[0x31c] |= 0x10000u; + state = DecodeState(queue); + Require(!state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an unclamped UNORM target with BLEND_BYPASS was refused or blended"); + queue = target(0x0000010cu); + Require(DecodeState(queue).colors.at(0).format == VK_FORMAT_R8G8_SNORM, "an unclamped SNORM target that does not blend was refused"); + queue.context[0x1e0] = blending; + expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented"); + queue = target(0x00000604u); + expectFailure([&] { DecodeState(queue); }, "unclamped normalized color is unsupported"); + queue = target(0x0000800cu); + queue.context[0x1e0] = blending; + Require(DecodeState(queue).blends.at(0).blendEnable, "a clamped UNORM target stopped blending"); + queue = target(0x00000708u); + queue.context[0x1e0] = blending; + state = DecodeState(queue); + Require(state.colors.at(0).format == VK_FORMAT_R16_SFLOAT && state.blends.at(0).blendEnable, "a 16 float target, which keeps BLEND_CLAMP clear, stopped blending"); +} + void OpaqueDestinationAlphaTests() { auto queue = makeState(); for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; @@ -1637,6 +1736,35 @@ void ReversedBlendRoutingTests() { Require(native == 121u && routed == 932u, "the reversed-order blend routing accepts " + std::to_string(native) + " native and " + std::to_string(routed) + " routed factor combinations, not 121 and 932"); } +void Color565TargetTests() { + auto queue = makeState(); + const auto info = [&](std::uint32_t number, std::uint32_t swap) { + queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (16u << 2u) | (number << 8u) | (swap << 11u); + }; + info(0, 2); + const auto reversed = AgcDriver::Graphics::DecodeState(queue); + Require(reversed.colors.size() == 1 && reversed.colors[0].format == VK_FORMAT_R5G6B5_UNORM_PACK16 && reversed.colors[0].elementBytes == 2 && reversed.colors[0].componentMapping == 0xe4u, "a 5_6_5 target with the reversed component order did not render through R5G6B5 with X in its high bits"); + Require(reversed.blends[0].colorWriteMask == 0xfu, "a 5_6_5 target with the reversed component order did not write all its channels"); + queue.context[0x8e] = 0x1u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_R_BIT, "a 5_6_5 target with the reversed component order writing only X did not write the R5G6B5 channel in its high bits"); + info(0, 0); + const auto standard = AgcDriver::Graphics::DecodeState(queue); + Require(standard.colors[0].format == VK_FORMAT_R5G6B5_UNORM_PACK16 && standard.colors[0].componentMapping == 0xc6u, "a 5_6_5 target with the standard component order did not render through R5G6B5 with X and Z exchanged"); + Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xc6u, "a 5_6_5 target with the standard component order did not export through the X and Z exchange"); + Require(standard.blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 5_6_5 target with the standard component order writing only X did not write the R5G6B5 channel in its low bits"); + queue.context[0x8e] = 0xfu; + info(0, 2); + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + queue.context[0x1e0] = 0x40000504u; + const auto blended = AgcDriver::Graphics::DecodeState(queue); + Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 5_6_5 target was not decoded"); + queue.context[0x1e0] = 0; + for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { + info(number, swap); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 16 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } +} + void Color1555TargetTests() { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u); @@ -1886,7 +2014,12 @@ void DepthStencilTests() { queue.context[0x000] = 0x23; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && clears(state.stencilFront), "a combined depth and stencil clear draw does not clear both planes"); - for (const auto control : {4u, 8u}) { + for (const auto select : {0xc0u, 0xf00u}) { + queue.context[0x000] = 0x23u | select; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && clears(state.stencilFront), "a copy sample selection without a copy changed a clear draw"); + } + for (const auto control : {4u, 8u, 0x84u, 0x108u}) { queue.context[0x000] = 0x22u | control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); } @@ -1895,7 +2028,23 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane"); queue.context[0x010] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected clear flags without depth/stencil planes"); + queue = makeState(); + queue.context[0x010] = 0x80000180; + queue.context[0x011] = 0x20000180; + queue.context[0x200] = 0x776; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto colorOnly = AgcDriver::Graphics::DecodeState(queue); + for (const auto control : {1u, 2u, 3u}) { + queue.context[0x000] = control; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest, "stale clear flags enabled absent depth/stencil planes"); + Require(state.color.address == colorOnly.color.address && state.blends[0].colorWriteMask == colorOnly.blends[0].colorWriteMask && state.viewport.minDepth == colorOnly.viewport.minDepth && state.viewport.maxDepth == colorOnly.viewport.maxDepth, "stale clear flags changed color writes or viewport depth"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a color-only draw with stale clear flags: " + AgcDriver::Graphics::DrawRejection(queue, false)); + } queue = makeState(); queue.context[0x31b] = 1u << 26u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); @@ -1936,6 +2085,12 @@ void DepthStencilTests() { 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"); + { + auto mipmapped = queue; + mipmapped.context[0x3b0] |= 2u << 28u; + mipmapped.context[0x31b] = (1u << 26u) | (1u << 13u) | 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeColorBuffer(mipmapped.context, 0); }, "slot 0, 64x4x4, viewMip 1, maxMip 2, slices 1..1, CB_COLOR_INFO 0x00028028, ATTRIB2 0x200fc003, ATTRIB3 0x0a000003, VIEW 0x04002001"); + } queue.context[0x31b] = 4u | (4u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface"); queue.context[0x31b] = 2u | (1u << 13u); @@ -2044,7 +2199,7 @@ void memoryValueFillTests() { } void depthMaintenanceTests() { - for (const auto mode : {0x4u, 0x8u, 0x80u, 0x100u, 0x1000u, 0x4000u}) { + for (const auto mode : {0x4u, 0x8u, 0x84u, 0x108u, 0xf88u, 0x1000u, 0x4000u}) { for (const auto clear : {0u, 1u, 2u, 3u}) { auto queue = makeState(); queue.context[0x000] = mode | clear; @@ -2058,7 +2213,7 @@ void depthMaintenanceTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } } - for (const auto control : {0u, 1u, 2u, 3u, 0x20u, 0x40u, 0x2000u, 0x2063u}) { + for (const auto control : {0u, 1u, 2u, 3u, 0x20u, 0x40u, 0x80u, 0xc0u, 0x100u, 0xf00u, 0x2000u, 0x2063u, 0x2fe3u}) { auto queue = makeState(); queue.context[0x000] = control; Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).empty(), "ordinary depth controls were mistaken for maintenance"); @@ -2629,6 +2784,67 @@ void multisampleTests() { expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid"); gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_2_BIT; Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a supported 2x2 sample location grid was refused"); + queue.context[0x31d] = 0; + state = DecodeState(queue); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.customSampleLocations && state.sampleLocationGrid, "2x checkerboard rasterization into a one-sample target (PPSA27836) did not rasterize sample 0 alone"); + const auto sampleZero = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedSampleZero{{{0.75f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}}}; + for (std::size_t pixel = 0; pixel < expectedSampleZero.size(); ++pixel) Require(sampleZero[pixel].x == expectedSampleZero[pixel].x && sampleZero[pixel].y == expectedSampleZero[pixel].y, "a one-sample target did not take sample 0's location in each pixel of the grid"); + gridContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + gridContext.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "VK_EXT_sample_locations"); + gridContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_1_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid"); + gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_1_BIT; + Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "one-sample grid locations were refused on a device that supports them"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + state = DecodeState(queue); + Require(state.samples == 1 && state.customSampleLocations && !state.sampleLocationGrid, "a uniform 2x pattern into a one-sample target did not use sample 0's location"); + auto stenciled = queue; + for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) stenciled.context[0x2fe + pixel * 4u] = word; + for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x10bu, 0u}, std::pair{0x10cu, 0x01ffff00u}, std::pair{0x10du, 0x01ffff00u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word; + state = DecodeState(stenciled); + Require(state.samples == 1 && state.depth && state.depth->samples == 1 && state.stencilTest && state.sampleLocationGrid && state.colors.size() == 1 && state.colors[0].samples == 1, "2x checkerboard rasterization into one-sample stencil and color targets (PPSA27836) did not rasterize sample 0 alone"); + stenciled.context[0x31d] = 0x9000; + expectFailure([&] { DecodeState(stenciled); }, "a color target's sample count differs from the rasterizer's"); + stenciled.context[0x31d] = 0; + stenciled.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + stenciled.context[0x1b4] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + expectFailure([&] { DecodeState(stenciled); }, "the depth target's sample count differs from the rasterizer's"); + const auto withInputs = [](AgcDriver::QueueState base, std::initializer_list inputs) { + for (const auto input : inputs) { + base.context[0x1b3] |= ShaderRecompiler::PixelInputBit(input); + base.context[0x1b4] |= ShaderRecompiler::PixelInputBit(input); + } + return base; + }; + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid, ShaderRecompiler::PixelInput::SampleCoverage}) expectFailure([&] { DecodeState(withInputs(queue, {input})); }, "sample count differs from the rasterizer's"); + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::LinearSample, ShaderRecompiler::PixelInput::Ancillary}) { + state = DecodeState(withInputs(queue, {input})); + Require(state.samples == 1 && !state.perSampleShading, "a sample or sample-index input into a one-sample target did not rasterize sample 0 alone"); + } + for (const auto position : {ShaderRecompiler::PixelInput::PositionX, ShaderRecompiler::PixelInput::PositionY}) expectFailure([&] { DecodeState(withInputs(queue, {ShaderRecompiler::PixelInput::Ancillary, position})); }, "sample count differs from the rasterizer's"); + auto iterated = queue; + iterated.context[0x293] = 0x06030000u; + state = DecodeState(iterated); + Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into a one-sample target (PPSA27836) did not rasterize sample 0 alone"); + state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::PositionX, ShaderRecompiler::PixelInput::PositionY})); + Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE with sample and position inputs (PPSA27836) did not rasterize sample 0 alone"); + state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid})); + Require(state.samples == 1, "PS_ITER_SAMPLE with centroid inputs did not rasterize sample 0 alone"); + expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::SampleCoverage})); }, "sample count differs from the rasterizer's"); + expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::PositionX})); }, "sample count differs from the rasterizer's"); + auto iteratedStencil = withInputs(stenciled, {}); + iteratedStencil.context[0x1b3] &= ~ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + iteratedStencil.context[0x1b4] &= ~ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + state = DecodeState(withInputs(iteratedStencil, {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::Ancillary})); + Require(state.samples == 1 && state.depth && state.depth->samples == 1, "sample and sample-index inputs into a one-sample depth target (PPSA07411) did not rasterize sample 0 alone"); + iteratedStencil.context[0x293] = 0x06030000u; + state = DecodeState(iteratedStencil); + Require(state.samples == 1 && state.depth && state.depth->samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into one-sample depth and color targets did not rasterize sample 0 alone"); + auto singleScan = queue; + singleScan.context[0x292] = 2; + expectFailure([&] { DecodeState(singleScan); }, "a color target's sample count differs from the rasterizer's"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x292] = 2; @@ -2769,10 +2985,21 @@ void multisampleTests() { queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); queue.context[0x31d] = 0; - expectFailure([&] { DecodeState(queue); }, "sample count differs"); + queue.context[0x293] = 0x10000u; + Require(DecodeState(queue).samples == 1 && !DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample target did not rasterize sample 0 alone"); queue = multisampled(); queue.context[0x292] = 2; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE"); + state = DecodeState(queue); + Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.customSampleLocations, "a 2x target rasterized without MSAA_ENABLE did not rasterize the target's samples"); + const auto aliased = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + queue = multisampled(); + queue.context[0x292] = 2; + queue.context[0x302] = 0x44cc; + state = DecodeState(queue); + Require(state.customSampleLocations && !state.sampleLocationGrid, "a 2x target rasterized without MSAA_ENABLE kept the per-pixel sample location grid of its centred samples"); queue = multisampled(); queue.context[0x2f8] |= 0x10; expectFailure([&] { DecodeState(queue); }, "coverage conversion"); @@ -2799,8 +3026,9 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeState(queue); }, "DCC over a multisampled color target is unsupported"); - expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeState(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); + expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); @@ -2845,7 +3073,8 @@ void multisampleTests() { expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); queue = rasterizedResolve; queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; @@ -2862,13 +3091,15 @@ void multisampleTests() { queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; queue.context[0x31c] = 0x1002e028; queue.context[0x31d] = 0x00012000; + queue.context[0x31e] = 0x00100038u; queue.context[0x325] = 0x2000; queue.context[0x323] = 0x3f800000; queue.context[0x324] = 0x3f800000; @@ -2876,6 +3107,9 @@ void multisampleTests() { auto fmaskPass = DecodeColorMetadataPass(queue); Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); + Require(fmaskPass->targets[0].dccControl == 0x00100038u, "FMASK decompress lost its DCC control"); + queue.context[0x31e] |= 0x80u; + Require(DecodeColorMetadataPass(queue)->targets[0].dccControl == 0x001000b8u, "FMASK decompress hid an unsupported DCC control from the importer"); queue = makeState(); queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; @@ -2987,6 +3221,143 @@ void multisampleTests() { 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 multisampleDccStateTests() { + static std::array color{}; + const auto address = (reinterpret_cast(color.data()) + 65535u) & ~std::uintptr_t{65535u}; + for (const auto log2 : {1u, 2u, 3u}) { + auto queue = makeState(); + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + queue.context[0x31c] |= 0x10000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + queue.context[0x31e] = 0x00100038u; + queue.context[0x325] = 0x1234u; + queue.context[0x3b8] = 0x4dc6c000u; + queue.context[0x2f8] = (log2 << 20u) | log2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0; + queue.context[0x2ff + pixel * 4u] = 0; + } + queue.context[0x292] = 3; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors.front().samples == (1u << log2) && state.colors.front().dccAddress == 0x123400u && state.colors.front().dccPipeAligned, "normal-render multisampled DCC state did not decode"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "normal-render multisampled DCC state failed the draw precheck"); + auto changed = queue; + changed.context[0x31e] |= 0x80u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color transforms or lossy"); + changed = queue; + changed.context[0x3b8] &= ~0x40000000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x325] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b8] |= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b0] |= 1u << 28u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips"); + } +} + +void multisampledDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto singleSampleScan = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0; + queue.context[0x292] = 2; + queue.context[0x31d] = 0x12000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x2290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + return queue; + }; + const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) { + Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations && !state.sampleLocationGrid && !state.perSampleShading, what + " did not rasterize the depth target's samples"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center"); + }; + const auto depthOnly = [&] { + auto queue = singleSampleScan(); + queue.context[0x8e] = 0; + queue.context[0x010] = 0xa290098b; + queue.context[0x200] = 0x777; + queue.context[0x201] = 0; + return queue; + }; + auto queue = depthOnly(); + auto state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only single-sample scan decoded a color target"); + requireCentered(state, "a single-sample scan into a 4x depth target (PPSA18216's registers)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a single-sample scan into a 4x depth target"); + for (const auto primitive : {5u, 6u, 7u, 17u}) { + queue = depthOnly(); + queue.userConfig[0x242] = primitive; + requireCentered(DecodeState(queue), "a single-sample scan of primitive type " + std::to_string(primitive) + " into a 4x depth target"); + } + queue = depthOnly(); + queue.context[0x000] = 1; + queue.context[0x00b] = std::bit_cast(0.25f); + state = DecodeState(queue); + requireCentered(state, "a depth clear scanned single-sampled into a 4x depth target"); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS, "a depth clear into a 4x depth target did not write every covered sample"); + queue = depthOnly(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + requireCentered(DecodeState(queue), "a single-sample scan whose pixel program is skipped"); + queue = depthOnly(); + queue.context[0x201] = 0x00040000u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)"); + queue = singleSampleScan(); + state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "a single-sample scan into 4x depth and color targets lost its color target"); + requireCentered(state, "a single-sample scan into 4x depth and color targets"); + queue.context[0x293] = 0x10000u; + Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan"); + queue = singleSampleScan(); + queue.context[0x31d] = 0; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x010] = 0x22900983; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with alpha-to-coverage"); + for (const auto primitive : {2u, 3u}) { + queue = singleSampleScan(); + queue.userConfig[0x242] = primitive; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles"); + } + queue = singleSampleScan(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index"); + queue = singleSampleScan(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample); + requireCentered(DecodeState(queue), "a single-sample scan with a pixel program reading sample-interpolated inputs"); + queue = singleSampleScan(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a sample-mask export"); + queue = singleSampleScan(); + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + void clipDistanceTests() { auto queue = makeState(); queue.context[0x207] = 0x0040000f; @@ -3362,9 +3733,36 @@ struct MockDescriptorWrite { std::vector images; }; +struct LiveCounter { + std::atomic value{0}; + LiveCounter() = default; + LiveCounter(const LiveCounter& other) : value(other.value.load()) {} + LiveCounter& operator=(const LiveCounter& other) { + value.store(other.value.load()); + return *this; + } + operator std::int64_t() const { return value.load(); } + LiveCounter& operator++() { + ++value; + return *this; + } + LiveCounter& operator--() { + --value; + return *this; + } + LiveCounter& operator+=(std::int64_t count) { + value += count; + return *this; + } + LiveCounter& operator-=(std::int64_t count) { + value -= count; + return *this; + } +}; + struct MockVulkan { std::uint64_t next = 1; - std::int64_t live = 0; + LiveCounter live; std::map bufferSizes; std::map bufferUsage; std::map allocationFlags; @@ -3384,6 +3782,7 @@ struct MockVulkan { std::vector poolSizes; std::uint32_t poolMaxSets = 0; VkDescriptorPoolCreateFlags poolFlags = 0; + std::uint32_t freedDescriptorSets = 0; struct DescriptorPool { VkDescriptorPoolCreateFlags flags; std::uint32_t maxSets; @@ -3416,6 +3815,11 @@ struct MockVulkan { }; MockVulkan mock; +std::mutex pipelineCreateMutex; +std::condition_variable pipelineCreateCondition; +bool blockLibraryCreate = false; +bool libraryCreateEntered = false; +bool releaseLibraryCreate = false; template THandle makeHandle() { @@ -3546,6 +3950,15 @@ VKAPI_ATTR VkResult VKAPI_CALL mockResetDescriptorPool(VkDevice, VkDescriptorPoo return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool handle, std::uint32_t count, const VkDescriptorSet*) { + auto& pool = mock.descriptorPools.at(handle); + Require((pool.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT) != 0, "a descriptor set was freed from a pool without free support"); + Require(count <= pool.allocated, "more descriptor sets were freed than allocated"); + pool.allocated -= count; + mock.freedDescriptorSets += count; + return VK_SUCCESS; +} + VKAPI_ATTR VkResult VKAPI_CALL mockCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) { *sampler = makeHandle(); mock.samplerFlags[*sampler] = info->flags; @@ -3600,7 +4013,23 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipeli } 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"); + { + std::unique_lock lock(pipelineCreateMutex); + if (blockLibraryCreate) { + libraryCreateEntered = true; + pipelineCreateCondition.notify_all(); + pipelineCreateCondition.wait(lock, [] { return releaseLibraryCreate; }); + } + } + Require(count == 1, "mock expects exactly one graphics pipeline per call"); + if ((infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) { + const auto* linked = static_cast(infos[0].pNext); + Require(linked != nullptr && linked->sType == VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR && linked->libraryCount != 0, "mock expects a graphics pipeline library or a pipeline linked from libraries"); + *pipelines = makeHandle(); + ++mock.pipelineCreateCount; + ++mock.live; + return VK_SUCCESS; + } 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; @@ -3715,6 +4144,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkCreateDescriptorPool", reinterpret_cast(mockCreateDescriptorPool)}, {"vkDestroyDescriptorPool", reinterpret_cast(mockDestroyDescriptorPool)}, {"vkAllocateDescriptorSets", reinterpret_cast(mockAllocateDescriptorSets)}, + {"vkFreeDescriptorSets", reinterpret_cast(mockFreeDescriptorSets)}, {"vkResetDescriptorPool", reinterpret_cast(mockResetDescriptorPool)}, {"vkCreateSampler", reinterpret_cast(mockCreateSampler)}, {"vkDestroySampler", reinterpret_cast(mockDestroySampler)}, @@ -3893,12 +4323,62 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st ShaderRecompiler::RecompileResult fragment; vertex.bindings.push_back(binding); mock = MockVulkan{}; - const auto context = mockContext(); + auto context = mockContext(); + context.emptyBuffer = reinterpret_cast(std::uintptr_t{0x1000}); const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); } Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); } +void oversizedDescriptorTests() { + constexpr std::size_t blockBytes = 65536; + constexpr std::size_t tailBytes = 8192; + void* block = ::operator new(blockBytes, std::align_val_t{blockBytes}); + std::memset(block, 0x5a, blockBytes); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + const auto oversized = [](const void* pointer) { return makeBinding(Role::GuestBuffers, 0, 1, vsharp(pointer, 0xffffffffu)); }; + const auto* tail = static_cast(block) + blockBytes - tailBytes; + { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(oversized(tail)); + mock = MockVulkan{}; + const auto context = mockContext(); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + const auto& write = findWrite(0); + Require(write.buffers.size() == 1 && write.buffers[0].range == tailBytes && sameBytes(bufferBytes(write.buffers[0].buffer), tail, tailBytes), "a V# past the range limit was not bound up to the end of its registered allocation"); + } + Require(mock.live == 0, "a V# bound up to its registered allocation leaked Vulkan objects"); + } + expectSingleFailure(oversized(block), "descriptor range limit"); + expectSingleAccepted(oversized(reinterpret_cast(0x1000)), "an unregistered V# past the range limit"); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + ::operator delete(block, std::align_val_t{blockBytes}); +} + +void guestBufferViewBytesTests() { + using AgcDriver::Graphics::GuestBufferMemory; + for (const std::uint64_t bytes : {2ull, 3ull, 4ull, 6ull, 7ull, 16ull, 4096ull, 4098ull}) { + for (const std::uint32_t adjustment : {0u, 4u, 16u, 256u}) { + Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == bytes + adjustment, "a view whose adjustment is a whole number of DWORDs did not add the adjustment"); + } + for (const std::uint32_t adjustment : {1u, 2u, 3u, 5u, 7u}) { + const auto expected = 4 * ((adjustment + bytes + 3) / 4); + Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == expected, "an off-DWORD view did not cover every byte in its range"); + } + } +} + void descriptorLayoutFlagTests() { constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; mock = MockVulkan{}; @@ -4075,6 +4555,152 @@ void descriptorSnapshotTests() { } } +alignas(16) std::array guestMovedFirst{0x51515151, 0x52525252, 0x53535353, 0x54545454, 0x55555555, 0x56565656, 0x57575757, 0x58585858}; +alignas(16) std::array guestMovedSecond{0x61616161, 0x62626262, 0x63636363, 0x64646464, 0x65656565, 0x66666666, 0x67676767, 0x68686868}; + +void dispatchMoveTests() { + using AgcDriver::Graphics::CompiledShader; + using AgcDriver::Graphics::DescriptorCache; + using AgcDriver::Graphics::Recorder; + using AgcDriver::Graphics::ShaderResources; + using Refusal = ShaderResources::MoveRefusal; + mock = MockVulkan{}; + mock.allowCopies = true; + auto context = mockContext(); + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + ShaderRecompiler::RecompileResult built; + auto inputs = makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))); + inputs.bufferWritten = {false, false}; + built.bindings.push_back(inputs); + auto output = makeBinding(Role::GuestBuffers, 4, 1, vsharp(guestThird.data(), 8)); + output.bufferWritten = {true}; + built.bindings.push_back(output); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0}; + ShaderResources resources(context, shader); + const auto moveTo = [&](std::size_t element, const void* where, std::uint32_t bytes) { + auto program = built; + const auto words = vsharp(where, bytes); + std::copy(words.begin(), words.end(), program.bindings[0].guestDescriptor.begin() + static_cast(element) * 4); + return program; + }; + const auto prove = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, ShaderResources::MoveProof& proof) { + return resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, maxBytes, proof); + }; + const auto refuses = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, Refusal why, const char* what) { + ShaderResources::MoveProof refusal; + const auto listed = prove(program, maxBytes, refusal); + Require(!listed.has_value() && refusal.refusal == why, what); + }; + + ShaderResources::MoveProof proof; + auto moved = prove(built, 0, proof); + Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "a dispatch with every buffer where it was built moved something"); + + const auto relocated = moveTo(1, guestMovedSecond.data(), 24); + const CompiledShader relocatedShader{ShaderRecompiler::ShaderStage::Compute, &relocated, 0}; + Require(ShaderResources::ContentKey(shader, false, true) == ShaderResources::ContentKey(relocatedShader, false, true), "moving a read-only buffer changed the movable content key"); + Require(ShaderResources::ContentKey(shader, false, false) != ShaderResources::ContentKey(relocatedShader, false, false), "the fixed content key ignored a moved buffer"); + + moved = prove(relocated, 0, proof); + Require(moved.has_value() && moved->size() == 1 && proof.refusal == Refusal::None && proof.buffers == 1 && proof.bytes >= 24, "a moved read-only buffer was not listed"); + Require(moved->front().address == reinterpret_cast(guestMovedSecond.data()) && moved->front().size == 24 && moved->front().words.empty(), "the listed buffer is not the moved range"); + + moved = prove(moveTo(0, guestFirst.data(), 8), 0, proof); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == reinterpret_cast(guestFirst.data()) && moved->front().size == 8, "a buffer that only changed its size was not listed"); + + auto both = relocated; + const auto firstWords = vsharp(guestMovedFirst.data(), 12); + std::copy(firstWords.begin(), firstWords.end(), both.bindings[0].guestDescriptor.begin()); + moved = prove(both, 1024, proof); + Require(moved.has_value() && moved->size() == 2 && proof.buffers == 2 && proof.bytes >= 36, "two moved buffers were not both listed"); + refuses(both, 30, Refusal::Limit, "moved buffers past the byte budget together were served"); + + auto written = relocated; + written.bindings[0].bufferWritten[1] = true; + refuses(written, 0, Refusal::Written, "a moved buffer the dispatch writes was served"); + refuses(moveTo(1, guestMovedSecond.data(), 20000), 0, Refusal::Written, "a moved buffer over the storage buffer range that no registered allocation holds was served"); + refuses(relocated, 16, Refusal::Limit, "a moved buffer over the byte budget was served"); + refuses(moveTo(1, reinterpret_cast(std::uintptr_t{0x10}), 24), 0, Refusal::Inaccessible, "a moved buffer over unmapped memory was served"); + refuses(moveTo(1, guestThird.data(), 8), 0, Refusal::Overlap, "a moved buffer over the dispatch's own written range was served"); + auto shaped = relocated; + shaped.bindings[0].guestDescriptor.resize(4); + refuses(shaped, 0, Refusal::Shape, "a dispatch with another binding shape was served"); + recorder.Sync(); + } + { + constexpr std::size_t blockBytes = 65536; + void* block = ::operator new(blockBytes, std::align_val_t{blockBytes}); + std::memset(block, 0x5a, blockBytes); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + const auto* end = static_cast(block) + blockBytes; + const auto strided = [](const void* pointer, std::uint32_t stride, std::uint32_t records) { + auto words = vsharp(pointer, records); + words[1] |= stride << 16u; + return words; + }; + DescriptorCache cache(context); + context.descriptorCache = &cache; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + const auto proveAgainst = [&](const ShaderRecompiler::RecompileResult& built, const std::vector& incoming, ShaderResources::MoveProof& proof, std::optional>& moved) { + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0}; + ShaderResources resources(context, shader); + auto program = built; + program.bindings[0].guestDescriptor = incoming; + moved = resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, 0, proof); + }; + const auto readOnly = [&](const std::vector& words) { + ShaderRecompiler::RecompileResult built; + auto binding = makeBinding(Role::GuestBuffers, 0, 1, words); + binding.bufferWritten = {false}; + built.bindings.push_back(binding); + return built; + }; + const auto address = [](const void* pointer) { return reinterpret_cast(pointer); }; + ShaderResources::MoveProof proof; + std::optional> moved; + + const auto* tail = end - 8192; + const auto* nearEnd = end - 4096; + const auto oversized = readOnly(vsharp(tail, 0xffffffffu)); + proveAgainst(oversized, vsharp(tail, 0xffffffffu), proof, moved); + Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "an unchanged V# the build cut to its allocation end was not an exact hit"); + proveAgainst(oversized, vsharp(nearEnd, 0xffffffffu), proof, moved); + Require(moved.has_value() && moved->size() == 1 && proof.buffers == 1 && proof.bytes == 4096 && moved->front().address == address(nearEnd) && moved->front().size == 4096, "a moved V# past the range limit was not snapshotted at its clamped range"); + proveAgainst(oversized, vsharp(nearEnd, 0xfffffff0u), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().size == 4096, "another oversized size over the same allocation end listed another range"); + proveAgainst(oversized, vsharp(tail, 4096), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(tail) && moved->front().size == 4096, "a V# that now fits the range limit was not listed at its own size"); + + proveAgainst(oversized, vsharp(block, 0xffffffffu), proof, moved); + Require(!moved.has_value() && proof.refusal == Refusal::Limit, "a moved V# the allocation holds past the range limit was served"); + + const auto overhang = readOnly(strided(tail, 24, 342)); + proveAgainst(overhang, strided(tail, 24, 342), proof, moved); + Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None, "an unchanged V# with a stride overhang past the allocation was not an exact hit"); + proveAgainst(overhang, strided(nearEnd, 24, 171), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(nearEnd) && moved->front().size == 4096 && proof.bytes == 4096, "a moved V# with a stride overhang was snapshotted past the allocation end"); + proveAgainst(overhang, strided(nearEnd, 24, 100), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().size == 2400, "a moved V# inside the allocation was not listed at its own size"); + recorder.Sync(); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + ::operator delete(block, std::align_val_t{blockBytes}); + } + Require(mock.live == 0, "dispatch move resources leaked Vulkan objects"); +} + void resourceTests() { mock = MockVulkan{}; const auto context = mockContext(); @@ -4196,8 +4822,8 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "descriptor range limit"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "an unregistered V# past the range limit"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "an unregistered V# past the range limit"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); @@ -4251,6 +4877,18 @@ void resourceTests() { expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 3, "detiler is unavailable"); expectStageResources(Role::GuestSamplers, Kind::Sampler, 4, 2, "shader sampler descriptors exceed per-stage limits"); } + { + mock = MockVulkan{}; + auto limited = mockContext(); + limited.limits.maxPerStageDescriptorSamplers = 16; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto samplers = makeBinding(Role::GuestSamplers, static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers), 17, std::vector(17u * 4u)); + samplers.kind = Kind::Sampler; + vertex.bindings.push_back(std::move(samplers)); + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, vertex, fragment, state.color, 0, 0); }, "shader sampler descriptors exceed per-stage limits: required 17, device limit 16"); + Require(mock.live == 0, "sampler limit rejection leaked Vulkan objects"); + } { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; @@ -4365,6 +5003,28 @@ void descriptorCacheTests() { expectFailure([&] { cache.Allocate(fittingLayout, fitting, false, 3u); }, "descriptor set exceeds the device's per-set descriptor limit"); } Require(mock.live == 0, "the combined descriptor cache leaked a pool or layout"); + mock = MockVulkan{}; + auto lavapipeContext = mockContext(); + lavapipeContext.individuallyFreeDescriptorSets = true; + lavapipeContext.limits.maxDescriptorSetStorageBuffers = 16; + { + AgcDriver::Graphics::DescriptorCache cache(lavapipeContext); + const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + const std::array key{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT}; + const auto layout = cache.Layout(key, std::span(&binding, 1)); + const std::array sizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}}}; + const auto first = cache.Allocate(layout, sizes); + const auto second = cache.Allocate(layout, sizes); + Require(first.pool == second.pool && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, "lavapipe chain pools do not enable individual descriptor-set frees"); + cache.Free(first); + Require(mock.freedDescriptorSets == 1 && mock.descriptorPools.at(first.pool).resets == 0, "freeing one of two live sets reset its chain pool"); + cache.Free(second); + Require(mock.freedDescriptorSets == 2 && mock.descriptorPools.at(second.pool).resets == 1, "freeing the last live set did not free it and reset its chain pool"); + const auto reused = cache.Allocate(layout, sizes); + Require(reused.pool == first.pool && mock.descriptorPools.at(reused.pool).attempts == 3, "an emptied lavapipe chain pool was not reused"); + cache.Free(reused); + } + Require(mock.live == 0, "the lavapipe descriptor cache leaked a pool or layout"); } void imageTableAllocationTests() { @@ -4632,21 +5292,33 @@ void storageTextureBudgetTests() { memory.memoryHeaps[1] = {32 * GiB, 0}; memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; - Require(StorageTextureBudget(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap"); + Require(StorageTextureBudget(memory, nullptr, 0, 0) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap"); reported.heapBudget[0] = 100 * GiB; reported.heapBudget[1] = 100 * GiB; - Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); reported.heapBudget[2] = 10 * GiB; - Require(StorageTextureBudget(memory, &reported) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + reported.heapUsage[2] = 5 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter"); + reported.heapUsage[2] = 8 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 1792 * MiB, "a 10 GiB budget with 5 GiB used outside the texture caches does not leave 1792 MiB after the sampled textures' 2 GiB floor and 1280 MiB of headroom"); + reported.heapUsage[2] = 9 * GiB; + Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved past their 2 GiB floor"); + reported.heapUsage[2] = 1 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use"); + Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom"); + reported.heapUsage[2] = 9 * GiB + 512 * MiB; + Require(StorageTextureBudget(memory, &reported, 2 * GiB, 512 * MiB) == 512 * MiB, "a heap whose other users leave no room does not keep the 512 MiB floor"); + reported.heapUsage[2] = 0; reported.heapBudget[2] = 6 * GiB; - Require(StorageTextureBudget(memory, &reported) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); reported.heapBudget[2] = 1 * GiB; - Require(StorageTextureBudget(memory, &reported) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); reported.heapBudget[2] = 40 * GiB; - Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; - Require(StorageTextureBudget(memory, &reported) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); } void sampledBudgetReportTests() { @@ -4720,6 +5392,8 @@ struct ModuleShape { std::uint32_t clipDistanceLength = 0; std::uint32_t cullDistanceLength = 0; bool layerOutput = false; + bool terminateInvocation = false; + bool omitTerminateExtension = false; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { @@ -4914,7 +5588,7 @@ std::vector makeModule(const ModuleShape& shape) { } emit(function, spv::OpFunction, {voidType, main, 0, functionType}); emit(function, spv::OpLabel, {label}); - emit(function, spv::OpReturn, {}); + emit(function, shape.terminateInvocation ? spv::OpTerminateInvocation : spv::OpReturn, {}); emit(function, spv::OpFunctionEnd, {}); std::vector words{spv::MagicNumber, 0x10300, 0, next, 0}; emit(words, spv::OpCapability, {spv::CapabilityShader}); @@ -4940,6 +5614,14 @@ std::vector makeModule(const ModuleShape& shape) { 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.terminateInvocation && !shape.omitTerminateExtension) { + const std::string extension = "SPV_KHR_terminate_invocation"; + 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); + } emit(words, spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450}); const auto entryPointOffset = words.size(); if (shape.vertexInput) emit(words, spv::OpEntryPoint, {spv::ExecutionModelVertex, main, 0x6e69616du, 0, output, input}); @@ -5013,6 +5695,55 @@ void pipelineLibrarySetTests() { } } +void pipelineLibraryClearWaitsForBuildTests() { + using namespace AgcDriver::Graphics; + 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 = 101; + fragment.variantId = 102; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{1}, fragmentKey{2}; + PipelineSetLayouts resources{}; + resources.main = makeHandle(); + resources.fragment = makeHandle(); + resources.mainKey = mainKey; + resources.fragmentKey = fragmentKey; + { + std::lock_guard lock(pipelineCreateMutex); + blockLibraryCreate = true; + libraryCreateEntered = false; + releaseLibraryCreate = false; + } + std::thread build([&] { Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "graphics pipeline library prebuild was skipped"); }); + { + std::unique_lock lock(pipelineCreateMutex); + pipelineCreateCondition.wait(lock, [] { return libraryCreateEntered; }); + } + std::atomic cleared{false}; + std::thread clear([&] { + ClearCachedPipelines(context.device); + cleared.store(true, std::memory_order_release); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const bool waited = !cleared.load(std::memory_order_acquire); + { + std::lock_guard lock(pipelineCreateMutex); + releaseLibraryCreate = true; + } + pipelineCreateCondition.notify_all(); + build.join(); + clear.join(); + Require(waited, "clearing pipeline libraries did not wait for an active part build"); + Require(cleared.load(std::memory_order_acquire), "clearing pipeline libraries did not complete after the part build"); + Require(mock.live == 0, "clearing during a library build leaked Vulkan objects"); +} + void barycentricEmulationTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -5285,11 +6016,14 @@ void rectListTests() { fragment.fragmentParameters = {{0, 0, false, false}, {1, 0, true, false}}; auto auxiliary = BuildRectListShaders(vertex, fragment, target); Require(!auxiliary.control.spirv.empty() && !auxiliary.evaluation.spirv.empty(), "rect-list parameter shaders are empty"); + Require(faultAuxiliary.lastVertexControl.Identity() == faultAuxiliary.control.Identity() && auxiliary.lastVertexControl.Identity() != auxiliary.control.Identity() && auxiliary.lastVertexControl.bindings.size() == 1 && auxiliary.lastVertexControl.bdaAbiVersion == auxiliary.control.bdaAbiVersion, "the last-vertex rect-list control shader does not differ from the first-vertex one exactly when a flat parameter is present"); auto parameterQueue = makeState(); parameterQueue.userConfig[0x242] = 17; const auto parameterState = DecodeState(parameterQueue); - const std::array parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &auxiliary.control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; - ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + for (const auto* control : {&auxiliary.control, &auxiliary.lastVertexControl}) { + const std::array parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } fragment.fragmentParameters[0].perVertex = true; auto explicitInterpolation = BuildRectListShaders(vertex, fragment, target); Require(!explicitInterpolation.control.spirv.empty() && !explicitInterpolation.evaluation.spirv.empty(), "rect-list shaders for an explicitly interpolated parameter are empty"); @@ -5529,7 +6263,7 @@ std::string interlockShape(std::span words) { return shape; } -std::string interlockExits(std::span words, std::size_t& kills) { +std::string interlockExits(std::span words, std::size_t& kills, std::size_t& terminates) { struct Block { std::vector ops; std::vector next; @@ -5547,7 +6281,7 @@ std::string interlockExits(std::span words, std::size_t& ki if (op == spv::OpLabel) label = words[at + 1]; if (op == spv::OpLabel && function == entry && first == 0) first = label; if (function != entry) { - if (op == spv::OpBeginInvocationInterlockEXT || op == spv::OpEndInvocationInterlockEXT || op == spv::OpKill) return "an interlock instruction or OpKill outside the entry point"; + if (op == spv::OpBeginInvocationInterlockEXT || op == spv::OpEndInvocationInterlockEXT || op == spv::OpKill || op == spv::OpTerminateInvocation) return "an interlock instruction, OpKill or OpTerminateInvocation outside the entry point"; continue; } if (label == 0) continue; @@ -5571,6 +6305,7 @@ std::string interlockExits(std::span words, std::size_t& ki if (op == spv::OpEndInvocationInterlockEXT && stage++ != 1) return "block " + std::to_string(current) + " ends the interlock outside it"; if ((op == spv::OpKill || op == spv::OpReturn || op == spv::OpReturnValue || op == spv::OpUnreachable || op == spv::OpTerminateInvocation) && stage != 2) return "an exit in block " + std::to_string(current) + " skips OpEndInvocationInterlockEXT"; if (op == spv::OpKill) ++kills; + if (op == spv::OpTerminateInvocation) ++terminates; } for (const auto next : blocks[current].next) { const auto [known, added] = state.emplace(next, stage); @@ -5619,9 +6354,57 @@ void orderedPixelShaderTests() { auto faulting = orderedPixelRequest(0x30600u, bdaInterlock, OrderedBdaPixelCode); faulting.target.supportedExtensions = bdaExtensions; faulting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; - std::size_t kills = 0; - const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills); - Require(exits.empty() && kills == 6u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill, not the valid-mask kill and 5 BDA fault kills" : exits)); + std::size_t kills = 0, terminates = 0; + const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills, terminates); + Require(exits.empty() && kills == 6u && terminates == 0u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill and " + std::to_string(terminates) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault kills" : exits)); + const std::array terminateExtensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_terminate_invocation"}; + auto terminating = faulting; + terminating.target.supportedExtensions = terminateExtensions; + const auto terminated = ShaderRecompiler::Recompile(terminating).spirv.Copy(); + std::size_t terminatedKills = 0, terminatedStops = 0; + const auto terminatedExits = interlockExits(terminated, terminatedKills, terminatedStops); + Require(terminatedExits.empty() && terminatedKills == 1u && terminatedStops == 5u, "a primitive-ordered pixel shader with BDA reads and SPV_KHR_terminate_invocation: " + (terminatedExits.empty() ? std::to_string(terminatedKills) + " OpKill and " + std::to_string(terminatedStops) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault terminations" : terminatedExits)); + std::size_t terminateDeclarations = 0; + for (std::size_t at = 5; at < terminated.size() && (terminated[at] >> 16u) != 0; at += terminated[at] >> 16u) { + if ((terminated[at] & 0xffffu) == spv::OpExtension && std::string_view(reinterpret_cast(&terminated[at + 1])) == "SPV_KHR_terminate_invocation") ++terminateDeclarations; + } + Require(terminateDeclarations == 1u, "the module declared SPV_KHR_terminate_invocation " + std::to_string(terminateDeclarations) + " times"); + const auto countOps = [](std::span words) { + std::map counts; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) ++counts[static_cast(words[at] & 0xffffu)]; + return counts; + }; + const std::array bdaCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + auto unordered = orderedPixelRequest(0x20600u, bdaCapabilities, OrderedBdaPixelCode); + unordered.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + unordered.target.supportedExtensions = terminateExtensions; + const auto unorderedOps = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words()); + Require(!unorderedOps.contains(spv::OpBeginInvocationInterlockEXT), "a pixel shader without PRIMITIVE_ORDERED_PIXEL_SHADER began the interlock"); + Require(unorderedOps.at(spv::OpTerminateInvocation) == 5u && unorderedOps.at(spv::OpKill) == 1u, "a pixel shader with BDA reads and SPV_KHR_terminate_invocation did not terminate on a fault"); + unordered.target.supportedExtensions = bdaExtensions; + const auto unorderedKills = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words()); + Require(!unorderedKills.contains(spv::OpTerminateInvocation) && unorderedKills.at(spv::OpKill) == 6u, "a pixel shader with BDA reads and no SPV_KHR_terminate_invocation did not kill on a fault"); + static constexpr std::array vertexBdaCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf80008cfu, 0x07070707u, 0xbf810000u}; + const std::array vertexCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; + const auto vertexWords = [&](std::span extensions) { + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Vertex, 0x10000u, vertexBdaCode, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = vertexCapabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return ShaderRecompiler::Recompile(request).spirv.Copy(); + }; + const auto vertexStops = vertexWords(bdaExtensions); + const auto vertexTerminate = vertexWords(terminateExtensions); + const auto vertexOps = countOps(vertexTerminate); + Require(std::ranges::equal(vertexStops, vertexTerminate) && !vertexOps.contains(spv::OpTerminateInvocation) && !vertexOps.contains(spv::OpKill) && vertexOps.at(spv::OpReturn) > 1u, "a vertex shader with BDA reads changed its fault exit when SPV_KHR_terminate_invocation was advertised"); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } @@ -6105,13 +6888,16 @@ void ProvokingVertexTests() { generated.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; ValidateProvokingVertex(unsupported, generated); } - for (std::uint32_t generated = 0; generated < 3; ++generated) { + for (std::uint32_t generated = 0; generated < 2; ++generated) { auto state = last; - if (generated == 0) state.rectList = true; - if (generated == 1) state.stages.mesh.emplace(); - if (generated == 2) state.stages.tessellation.emplace(); + if (generated == 0) state.stages.mesh.emplace(); + if (generated == 1) state.stages.tessellation.emplace(); expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for generated primitive pipelines"); } + auto rectangle = last; + rectangle.rectList = true; + rectangle.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + ValidateProvokingVertex(supported, rectangle, flatShaders); last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_MAX_ENUM_EXT; expectFailure([&] { ValidateProvokingVertex(supported, last); }, "invalid provoking vertex mode"); } @@ -6614,6 +7400,34 @@ void validationTests() { vertex.spirv = makeModule({.barycentric = true}); pixel.spirv = makeModule({.fragment = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "requires a fragment shader"); + const auto validateTerminate = [&](bool enabled) { + return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, false, false, enabled); + }; + vertex.spirv = makeModule({}); + pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true}); + validateTerminate(true); + expectFailure([&] { validateTerminate(false); }, "shaderTerminateInvocation"); + vertex.spirv = makeModule({.terminateInvocation = true}); + pixel.spirv = makeModule({.fragment = true}); + expectFailure([&] { validateTerminate(true); }, "fragment shader"); + vertex.spirv = makeModule({}); + pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true, .omitTerminateExtension = true}); + expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires SPV_KHR_terminate_invocation"); + expectFailure([&] { validateTerminate(false); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation"); + vertex.spirv = makeModule({.terminateInvocation = true, .omitTerminateExtension = true}); + pixel.spirv = makeModule({.fragment = true}); + expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation in a fragment shader"); + vertex.spirv = makeModule({}); + pixel.spirv = ShaderRecompiler::SpecializeSpirv(makeModule({.fragment = true, .terminateInvocation = true})); + std::size_t terminates = 0; + std::size_t declarations = 0; + for (std::size_t at = 5; at < pixel.spirv.size(); at += pixel.spirv[at] >> 16u) { + const auto op = pixel.spirv[at] & 0xffffu; + if (op == spv::OpTerminateInvocation) ++terminates; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&pixel.spirv[at + 1])) == "SPV_KHR_terminate_invocation") ++declarations; + } + Require(terminates == 1u && declarations == 1u, "specialization dropped OpTerminateInvocation or its extension declaration"); + validateTerminate(true); } { ShaderRecompiler::RecompileResult vertex; @@ -6921,6 +7735,16 @@ void stagingPoolBudgetTests() { Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{128} << 20u, "the staging pool budget on system memory fell below 128 MiB"); memory.memoryTypes[0] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1}; Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a heap with a device-only memory type was taken for system memory"); + memory.memoryHeaps[1].size = VkDeviceSize{16} << 30u; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{2} << 30u, "a zero reported budget did not fall back to an eighth of the heap"); + reported.heapBudget[0] = VkDeviceSize{1} << 30u; + reported.heapBudget[1] = VkDeviceSize{10} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{1280} << 20u, "a 10 GiB budget on the 16 GiB heap does not limit the staging pool to an eighth of the budget"); + reported.heapBudget[1] = VkDeviceSize{2} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{512} << 20u, "a 2 GiB budget does not keep the 512 MiB floor"); + reported.heapBudget[1] = VkDeviceSize{40} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{2} << 30u, "a budget above the heap size raised the staging pool past an eighth of the heap"); } void meshArgumentTests() { @@ -7188,6 +8012,320 @@ void ResourceContentKeyTests(bool benchmark) { } } +// The pipeline journal without a GPU: the recipe codec and the store key agree with each other and +// with the canonical state, identities follow the module and the specialization, the journal +// records each pipeline once, and a replay into the recording context (the lavapipe test replays +// into a second device) rebuilds the recorded graphics pipeline so the same configuration hits the +// store, hands the compute result to its hook, recovers a tail torn inside a record's padding and +// stops when cancelled. +void pipelineStoreLazyBufferPoolTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 201; + fragment.variantId = 202; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{31}, fragmentKey{32}; + const PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + Require(context.bufferPool == nullptr, "the lazy buffer pool fixture already owns a buffer pool"); + auto first = CachedPipeline(context, State{}, VertexInputLayout{}, resources, shaders); + const auto created = mock.pipelineCreateCount; + static_cast(GetBufferPool(context)); + auto again = CachedPipeline(context, State{}, VertexInputLayout{}, resources, shaders); + Require(again == first && mock.pipelineCreateCount == created, "creating the buffer pool dropped a cached pipeline of the live device"); + ClearCachedPipelines(context.device); + again.reset(); + first.reset(); + context.bufferPool.reset(); + Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects"); +} + +void pipelineJournalTests() { + using namespace AgcDriver::Graphics; + using Stage = ShaderRecompiler::ShaderStage; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(directory); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); + _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); + setenv("APS5_PIPELINE_JOURNAL_THREADS", "2", 1); +#endif + Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set"); + const auto module = [](std::uint32_t salt) { + ShaderRecompiler::RecompileResult result; + result.spirv = std::vector{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt}; + result.runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version; + return result; + }; + { + auto first = module(1); + const auto& constFirst = first; + auto copy = first; + const auto& constCopy = copy; + Require(!constFirst.Identity().Empty() && constFirst.Identity() == constCopy.Identity() && constFirst.spirv.data() == constCopy.spirv.data(), "a copied result did not share its module and identity"); + copy.spirv[8] = 99; + Require(!(constCopy.Identity() == constFirst.Identity()) && constCopy.spirv.data() != constFirst.spirv.data() && constFirst.Identity() == module(1).Identity(), "writing a copied module did not give it an identity of its own"); + auto specialized = module(1); + specialized.specialization.push_back({3, 4}); + Require(!(specialized.Identity() == constFirst.Identity()), "specialization constants did not enter the identity"); + auto subgroup = module(1); + subgroup.hostSubgroupSize = 64; + Require(!(subgroup.Identity() == constFirst.Identity()), "the host subgroup size did not enter the identity"); + Require(ShaderRecompiler::RecompileResult{}.Identity().Empty(), "a result without a module has an identity"); + } + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.tessellationShader = true; + context.limits.maxTessellationPatchSize = 32; + context.limits.maxColorAttachments = 8; + context.limits.maxPushConstantsSize = 256; + context.limits.maxVertexInputBindings = 16; + context.limits.maxVertexInputAttributes = 16; + context.limits.maxFramebufferWidth = 4096; + context.limits.maxFramebufferHeight = 4096; + context.limits.maxFramebufferLayers = 1; + std::uint64_t replayedComputeIdentityLow = 0; + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + auto vertex = module(1); + vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector(4))); + auto fragment = module(2); + fragment.pushConstants.resize(8); + fragment.bindings.push_back(makeBinding(Role::ShaderData, 0, 1, std::vector(4))); + fragment.bindings.back().descriptorSet = ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; + const std::array shaders{{{Stage::Vertex, &vertex, 0}, {Stage::Fragment, &fragment, PipelinePushSlotBytes}}}; + State state{}; + state.stages = {ShaderPath::Vertex, 7u, 64u, 32u, std::nullopt, std::nullopt}; + state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3, 3, 2, 1, 4, 128, 16, true}; + ColorTarget color{}; + color.address = 0x1000; + color.extent = {64, 32}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; + color.bytes = 64 * 32 * 4; + color.componentMapping = 0xec; + state.colors = {color, color}; + state.colors[1].format = VK_FORMAT_B8G8R8A8_UNORM; + state.colors[1].exportIndex = 1; + state.color = color; + state.hasColorTarget = true; + VkPipelineColorBlendAttachmentState blend{}; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorWriteMask = 0xf; + state.blends = {blend, blend}; + state.blend = blend; + state.blendConstants = {0.25f, 0.5f, 0.75f, 1.0f}; + DepthTarget depth{}; + depth.address = 0x2000; + depth.format = VK_FORMAT_D32_SFLOAT; + depth.samples = 1; + state.depth = depth; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + state.stencilTest = true; + state.stencilFront = {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xff, 0xff, 1}; + state.stencilBack = state.stencilFront; + state.renderExtent = {64, 32}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + state.viewport = {0, 32, 64, -32, 0, 1}; + state.scissor = {{0, 0}, {64, 32}}; + state.cullMode = VK_CULL_MODE_BACK_BIT; + state.frontFace = VK_FRONT_FACE_CLOCKWISE; + state.samples = 1; + const VertexInputLayout input{{{0, 16, VK_VERTEX_INPUT_RATE_VERTEX}}, {{0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}}}; + const auto layouts = DescribeSetLayouts(context, shaders); + Require(layouts.layouts.split, "a fragment stage with the fragment set did not split the set layouts"); + const auto recipe = MakeGraphicsPipelineRecipe(state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(recipe.stages.size() == 2 && recipe.stages[0].identity == vertex.Identity() && recipe.stages[1].identity == fragment.Identity() && !recipe.stages[0].generated && recipe.stages[1].pushConstantOffset == PipelinePushSlotBytes && recipe.splitSets, "the recipe does not name its stages"); + Require(recipe.state.colors.size() == 2 && recipe.state.colors[0].address == 0 && recipe.state.colors[0].componentMapping == 0xec && recipe.state.depth && recipe.state.depth->address == 0 && recipe.state.viewport.width == 0 && recipe.state.stages.registerValue == 0 && recipe.state.stages.tessellation && recipe.state.stages.tessellation->breakAtInstance, "the canonical state kept or dropped the wrong fields"); + const auto bytes = EncodeGraphicsPipelineRecipe(recipe); + GraphicsPipelineRecipe decoded; + Require(DecodeGraphicsPipelineRecipe(bytes, decoded), "the recipe does not decode"); + Require(EncodeGraphicsPipelineRecipe(decoded) == bytes, "the recipe does not round-trip byte for byte"); + for (std::size_t length = 0; length < bytes.size(); length += 7) { + GraphicsPipelineRecipe scratch; + Require(!DecodeGraphicsPipelineRecipe(std::span(bytes).first(length), scratch), "a truncated recipe decoded"); + } + std::vector original, canonical, replayed, moved, mapped; + BuildPipelineStoreKey(original, context, state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(canonical, context, recipe.state, recipe.vertexInput, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey}; + BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout); + Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline"); + auto gridState = state; + gridState.samples = 2; + gridState.customSampleLocations = true; + gridState.sampleLocationGrid = true; + gridState.perSampleShading = true; + gridState.sampleLocations[31] = 0x5a; + GraphicsPipelineRecipe gridDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), gridDecoded), "the sample grid recipe does not decode"); + Require(gridDecoded.state.sampleLocationGrid && gridDecoded.state.perSampleShading && gridDecoded.state.sampleLocations[31] == 0x5a, "the recipe dropped the sample grid, per-sample shading or the last sample location"); + std::vector grid, gridReplayed; + BuildPipelineStoreKey(grid, context, gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(gridReplayed, context, gridDecoded.state, gridDecoded.vertexInput, layouts.View(), shaders, gridDecoded.attachmentLayout); + Require(grid != original && grid == gridReplayed, "the decoded sample grid state keys another pipeline"); + auto lineState = state; + lineState.stages.mesh = ShaderRecompiler::MeshConfiguration{1, 2, 2, 4, 4, 32, 64, 1, 4, 2}; + GraphicsPipelineRecipe lineDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(lineState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), lineDecoded), "the line strip mesh recipe does not decode"); + Require(lineDecoded.state.stages.mesh && lineDecoded.state.stages.mesh->outputPrimitiveSize == 2u && lineDecoded.state.stages.mesh->esgsItemSize == 4u, "the recipe dropped the mesh output primitive size"); + auto fourSampleState = state; + fourSampleState.samples = 4; + auto maskState = fourSampleState; + maskState.quadSampleMask = {0x3u, 0x3u, 0x3u, 0x3u}; + maskState.stencilReplayReference = 7u; + maskState.reversedBlend = 1u; + GraphicsPipelineRecipe maskDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), maskDecoded), "the sample mask recipe does not decode"); + Require(maskDecoded.state.quadSampleMask == maskState.quadSampleMask && maskDecoded.state.stencilReplayReference.has_value() && maskDecoded.state.reversedBlend == 1u, "the recipe dropped the quad sample masks, the replayed stencil reference or the reversed blend"); + std::vector fourSamples, masked, maskReplayed; + BuildPipelineStoreKey(fourSamples, context, fourSampleState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(masked, context, maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(maskReplayed, context, maskDecoded.state, maskDecoded.vertexInput, layouts.View(), shaders, maskDecoded.attachmentLayout); + Require(masked != fourSamples && masked == maskReplayed, "the decoded sample mask state keys another pipeline"); + auto movedState = state; + movedState.viewport.x = 5; + movedState.colors[0].address = 0x9000; + movedState.renderLayers = 4; + BuildPipelineStoreKey(moved, context, movedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(moved == original, "a field the pipeline does not read changed the key"); + auto mappedState = state; + mappedState.colors[0].componentMapping = 0x24; + BuildPipelineStoreKey(mapped, context, mappedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(mapped != original, "the component mapping did not enter the key"); + std::vector unclamped, clamped, biased; + auto clampedState = state; + clampedState.depthBiasClamp = 1.0f; + BuildPipelineStoreKey(clamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(clamped == original, "a depth bias clamp without depth bias changed the key"); + clampedState.depthBias = true; + BuildPipelineStoreKey(clamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + clampedState.depthBiasClamp = 0.0f; + BuildPipelineStoreKey(unclamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(unclamped != original && clamped != unclamped && CanonicalPipelineState(clampedState).depthBias && CanonicalPipelineState(mappedState).depthBiasClamp == 0.0f, "the depth bias clamp did not enter the key"); + clampedState.depthBiasClamp = 2.0f; + BuildPipelineStoreKey(biased, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(biased == clamped, "the depth bias clamp's value entered the key"); + auto compute = module(3); + compute.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, std::vector(8))); + compute.pushConstants.resize(16); + compute.hostSubgroupSize = 32; + constexpr std::uint64_t fingerprint = 0x5eed'0000'0000'0001ull; + { + auto bound = StartPipelineJournalReplay(context, fingerprint, {}); + Require(bound != nullptr, "the first replay did not start"); + bound->Wait(); + Require(bound->Done() && PipelineJournalTotals().loadedRecipes == 0, "an absent journal loaded pipelines"); + } + const auto before = PipelineJournalTotals(); + RecordGraphicsPipeline(recipe, shaders); + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + const auto recorded = PipelineJournalTotals(); + Require(recorded.recordedGraphics == before.recordedGraphics + 1 && recorded.recordedCompute == before.recordedCompute + 1 && recorded.recordedModules == before.recordedModules + 3 && recorded.writeFailures == 0, "the journal did not record one graphics and one compute pipeline with three modules"); + RecordGraphicsPipeline(recipe, shaders); + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + const auto repeated = PipelineJournalTotals(); + Require(repeated.recordedGraphics == recorded.recordedGraphics && repeated.recordedCompute == recorded.recordedCompute && repeated.recordedModules == recorded.recordedModules, "a repeated pipeline was recorded again"); + Require(std::filesystem::file_size(PipelineJournalPath()) > 0, "the journal file is empty"); + const auto storeBefore = PipelineStoreTotals(); + std::vector receivedWords; + ShaderRecompiler::RecompileResult received; + bool receivedPush = false; + std::uint32_t computeCalls = 0; + PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + ++computeCalls; + received = shader; + receivedPush = pushConstants; + return true; + }}; + auto replay = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(replay != nullptr, "the replay did not start"); + replay->Wait(); + Require(replay->Done(), "the replay did not finish"); + const auto totals = PipelineJournalTotals(); + Require(totals.loadedRecipes == 2 && totals.loadedModules == 3 && totals.droppedRecords == 0, "the replay did not load the two recorded pipelines and three modules"); + Require(totals.replayedGraphics == 1 && totals.replayedCompute == 1 && totals.replayRejected == 0 && totals.replayFailed == 0, "the replay did not build one graphics and one compute pipeline"); + Require(computeCalls == 1 && receivedPush && received.Identity() == compute.Identity() && received.spirv == compute.spirv && received.bindings.size() == 1 && received.bindings[0].count == 2 && received.pushConstants.size() == 16 && received.hostSubgroupSize == 32, "the compute hook did not receive the recorded result"); + replayedComputeIdentityLow = received.Identity().low; + const auto storeReplayed = PipelineStoreTotals(); + Require(storeReplayed.misses == storeBefore.misses + 1 && storeReplayed.cached == storeBefore.cached + 1, "the replayed graphics pipeline did not enter the store"); + const auto created = mock.pipelineCreateCount; + bool built = true; + const auto pipeline = CachedPipeline(context, state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL, &built); + const auto storeHit = PipelineStoreTotals(); + Require(pipeline != nullptr && !built && mock.pipelineCreateCount == created && storeHit.hits == storeReplayed.hits + 1 && storeHit.misses == storeReplayed.misses, "the draw's configuration did not find the replayed pipeline"); + { + const auto journalPath = PipelineJournalPath(); + const auto whole = std::filesystem::file_size(journalPath); + Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned"); + std::filesystem::resize_file(journalPath, whole - 3); + auto later = module(4); + later.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector(4))); + RecordComputePipeline(later, false); + FlushPipelineJournal(); + Require(std::filesystem::file_size(journalPath) % 8 == 0 && PipelineJournalTotals().writeFailures == 0, "an append after a tail torn inside a record's padding left the file unaligned"); + auto torn = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(torn != nullptr, "the replay after the torn tail did not start"); + torn->Wait(); + const auto reloaded = PipelineJournalTotals(); + Require(reloaded.droppedRecords == 0 && reloaded.loadedModules == 4 && reloaded.loadedRecipes == 2 && reloaded.replayFailed == 0, "the journal did not recover from a tail torn inside a record's padding"); + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was"); + } + { + const auto before = PipelineJournalTotals(); + auto cancelled = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(cancelled != nullptr, "the third replay did not start"); + cancelled->Cancel(); + cancelled->Wait(); + const auto after = PipelineJournalTotals(); + Require(cancelled->Done() && after.replayFailed == 0 && after.replayRejected == 0 && after.replayedGraphics + after.replayPresent <= before.replayedGraphics + before.replayPresent + 1, "a cancelled replay did not stop cleanly"); + } + { + const std::uint32_t meshWithoutOutputSize = 2u; + std::fstream file(PipelineJournalPath(), std::ios::in | std::ios::out | std::ios::binary); + file.seekp(4); + file.write(reinterpret_cast(&meshWithoutOutputSize), sizeof(meshWithoutOutputSize)); + file.close(); + Require(file.good(), "the journal header could not be rewritten"); + auto stale = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(stale != nullptr, "the replay of a journal in the previous format did not start"); + stale->Wait(); + const auto discarded = PipelineJournalTotals(); + Require(discarded.loadedRecipes == 0 && discarded.loadedModules == 0, "a journal in format 2, whose mesh records lack the output primitive size, was loaded"); + } + ClearCachedPipelines(context.device); + } + Require(replayedComputeIdentityLow != 0, "the replayed compute identity is empty"); + Require(mock.live == 0, "the pipeline journal test leaked Vulkan objects"); +} + int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "200"); @@ -7197,10 +8335,29 @@ int main(int argc, char** argv) { setenv("APS5_HEAP_MIRROR_MIB", "4", 1); #endif try { + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-cache") { + descriptorCacheTests(); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unused-stencil-reference") { stencilReferenceUnusedTests(); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-journal") { + pipelineJournalTests(); + std::cout << "pipeline journal recipe, record and replay tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-clear-waits-for-build") { + pipelineLibraryClearWaitsForBuildTests(); + std::cout << "pipeline library clear waits for active part builds\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") { + pipelineStoreLazyBufferPoolTests(); + std::cout << "cached pipelines survive lazy buffer pool creation\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") { bool passed = false; { @@ -7278,6 +8435,11 @@ int main(int argc, char** argv) { std::cout << "Covered-centroid and multisample state checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--multisample-dcc-state") { + multisampleDccStateTests(); + std::cout << "Multisampled DCC state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; @@ -7311,8 +8473,10 @@ int main(int argc, char** argv) { RunGuestLeaseWaitTests(); ResourceContentKeyTests(false); stateTests(); + pipelineStoreLazyBufferPoolTests(); retainedPixelStageTests(); pipelineLibrarySetTests(); + pipelineLibraryClearWaitsForBuildTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); @@ -7337,8 +8501,10 @@ int main(int argc, char** argv) { ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); + Color565TargetTests(); Color1555TargetTests(); BlendBypassTests(); + UnclampedNormalizedColorTests(); CompactedExportTests(); ReversedComponentOrderTests(); ReversedComponentOrderBlendTests(); @@ -7352,9 +8518,11 @@ int main(int argc, char** argv) { uint8x4TargetTests(); multisampleTests(); MsaaEnableSingleSampleTests(); + multisampledDepthTests(); clipDistanceTests(); framebufferLayerTests(); UnwrittenSampleLocationTests(); + depthCopyFormatTests(); quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); @@ -7372,7 +8540,10 @@ int main(int argc, char** argv) { descriptorPoolTests(); pushConstantTests(); resourceTests(); + oversizedDescriptorTests(); + guestBufferViewBytesTests(); descriptorSnapshotTests(); + dispatchMoveTests(); descriptorCacheTests(); bufferBudgetPlacementTests(); imageTableAllocationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 84542f15f4..7f5b21682c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -94,6 +94,8 @@ struct SamplerCapture { const void* next = nullptr; VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM; VkSamplerCreateFlags flags = 0; + bool anisotropyEnable = false; + float maxAnisotropy = 0.0f; }; SamplerCapture samplerCapture; @@ -107,6 +109,8 @@ VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCre samplerCapture.created = true; samplerCapture.next = info->pNext; samplerCapture.flags = info->flags; + samplerCapture.anisotropyEnable = info->anisotropyEnable == VK_TRUE; + samplerCapture.maxAnisotropy = info->maxAnisotropy; const auto* reduction = static_cast(info->pNext); if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode; *sampler = fakeHandle(0x5a); @@ -174,14 +178,30 @@ void RunSamplerReductionTests(const Fields& base) { badFilterMode.filterMode = 3; rejectFields(badFilterMode, "unknown reduction filter mode 3"); Fields anisoReduction = base; - anisoReduction.filterMode = 1; - anisoReduction.xyMagFilter = 3; - anisoReduction.xyMinFilter = 3; anisoReduction.maxAnisoRatio = 2; anisoReduction.mipFilter = 1; - rejectFields(anisoReduction, "min or max reduction with anisotropic filtering"); + context.samplerAnisotropy = true; + context.limits.maxSamplerAnisotropy = 16.0f; + for (std::uint32_t mode = 1; mode < modes.size(); ++mode) { + for (const std::uint32_t filter : {2u, 3u}) { + anisoReduction.filterMode = mode; + anisoReduction.xyMagFilter = filter; + anisoReduction.xyMinFilter = filter; + const auto decoded = DecodeSamplerResource(pack(anisoReduction)); + Require(decoded.reductionMode == modes.at(mode) && decoded.anisotropyEnable && decoded.maxAnisotropy == 4.0f, "anisotropic min or max sampler decoded incorrectly"); + const auto created = createSampler(context, decoded); + Require(created.reductionMode == modes.at(mode), "an anisotropic min or max sampler did not chain its reduction mode"); + Require(created.anisotropyEnable && created.maxAnisotropy == 4.0f, "an anisotropic min or max sampler lost its anisotropy"); + Require(Sampler(context, decoded).RequiresFilterMinmax() == (filter == 3u), "anisotropic sampler min/max format requirement is wrong"); + } + } + context.samplerAnisotropy = false; + context.limits.maxSamplerAnisotropy = 1.0f; anisoReduction.filterMode = 0; Require(DecodeSamplerResource(pack(anisoReduction)).anisotropyEnable, "anisotropic weighted-average sampler was rejected"); + anisoReduction.filterMode = 1; + anisoReduction.mipFilter = 2; + rejectFields(anisoReduction, "min or max reduction with a linear mip filter"); auto compared = captured; compared.compareEnable = true; @@ -235,6 +255,11 @@ void RunSamplerReductionTests(const Fields& base) { maxLinearMip.filterMode = 2; maxLinearMip.mipFilter = 2; reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering"); + Fields minAnisoPoint = minPoint; + minAnisoPoint.xyMagFilter = 2; + minAnisoPoint.xyMinFilter = 2; + minAnisoPoint.maxAnisoRatio = 1; + reject([&] { pointFiltered(pack(minAnisoPoint)); }, "needs point filtering"); } void RunNonSeamlessCubeTests(const Fields& base) { diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 6a8e0d8e7a..34c28d4149 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -104,6 +104,10 @@ void RunGuestTextureResourceTests() { Fields nullType = base; nullType.typeRaw = 0; Require(IsNullTextureResource(pack(nullType)), "a type-0 texture with a nonzero base was not classified as null"); + nullType.typeRaw = 1; + Require(IsNullTextureResource(pack(nullType)), "a type-1 texture with a nonzero base was not classified as null"); + nullType.typeRaw = 2; + Require(!IsNullTextureResource(pack(nullType)), "a reserved type-2 texture was classified as null"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp b/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp new file mode 100644 index 0000000000..3b32c34c00 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp @@ -0,0 +1,134 @@ +#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; +using AgcDriver::HandoffSpinWindowFor; +using AgcDriver::LockSpinning; +using AgcDriver::SpinUntil; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckWindowPolicy() { + Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 usable processors do not spin for 150 us"); + Require(HandoffSpinWindowFor(nullptr, 9) == 150us, "9 usable processors do not spin for 150 us"); + Require(HandoffSpinWindowFor(nullptr, 8) == 0ns, "8 usable processors spin"); + Require(HandoffSpinWindowFor(nullptr, 4) == 0ns, "4 usable processors spin"); + Require(HandoffSpinWindowFor(nullptr, 0) == 0ns, "an unknown processor count spins"); + Require(HandoffSpinWindowFor("0", 20) == 0ns, "APS5_HANDOFF_SPIN_US=0 still spins"); + Require(HandoffSpinWindowFor("400", 2) == 400us, "APS5_HANDOFF_SPIN_US=400 does not set 400 us on 2 logical processors"); + Require(HandoffSpinWindowFor("-5", 20) == 0ns, "a negative window spins"); + Require(HandoffSpinWindowFor("text", 20) == 0ns, "a window that is no number spins"); + Require(HandoffSpinWindowFor("99999999999", 20) == 10000us, "a huge window is not limited to 10000 us"); +} + +void CheckUsableProcessors() { + const unsigned usable = AgcDriver::UsableProcessors(); + Require(usable >= 1, "no usable processor was counted"); + Require(usable <= (std::max)(std::thread::hardware_concurrency(), 64u), "more usable processors than the machine has"); +} + +void CheckSpinUntil() { + int calls = 0; + Require(SpinUntil(0ns, [&] { ++calls; return true; }) && calls == 1, "a ready condition was evaluated more than once"); + calls = 0; + Require(!SpinUntil(0ns, [&] { ++calls; return false; }) && calls == 1, "a zero window evaluated the condition more than once"); + calls = 0; + Require(SpinUntil(1s, [&] { return ++calls == 5; }) && calls == 5, "a condition that holds on its fifth evaluation was not seen"); + const auto start = std::chrono::steady_clock::now(); + Require(!SpinUntil(3ms, [] { return false; }), "a condition that never holds was reported ready"); + Require(std::chrono::steady_clock::now() - start >= 3ms, "the spin returned before its window ended"); + std::atomic flag{false}; + std::thread setter([&] { + std::this_thread::sleep_for(500us); + flag.store(true, std::memory_order_release); + }); + const bool seen = SpinUntil(5s, [&] { return flag.load(std::memory_order_acquire); }); + setter.join(); + Require(seen, "a flag set by another thread was not seen within the window"); +} + +template +void Contend(std::chrono::nanoseconds window, const char* what) { + constexpr int Workers = 4; + constexpr int Rounds = 20000; + TMutex mutex; + std::atomic inside{0}; + std::atomic overlapped{false}; + std::uint64_t counter = 0; + std::vector workers; + for (int worker = 0; worker < Workers; ++worker) { + workers.emplace_back([&] { + for (int round = 0; round < Rounds; ++round) { + LockSpinning(mutex, window); + if (inside.fetch_add(1) != 0) overlapped.store(true); + ++counter; + inside.fetch_sub(1); + mutex.unlock(); + } + }); + } + for (auto& worker : workers) worker.join(); + Require(!overlapped.load(), std::string(what) + ": two threads held the mutex at once"); + Require(counter == static_cast(Workers) * Rounds, std::string(what) + ": increments were lost"); +} + +void CheckBlockingFallback() { + std::mutex mutex; + std::atomic held{false}; + std::atomic released{false}; + std::thread holder([&] { + mutex.lock(); + held.store(true); + std::this_thread::sleep_for(20ms); + released.store(true); + mutex.unlock(); + }); + while (!held.load()) std::this_thread::yield(); + LockSpinning(mutex, 100us); + const bool afterRelease = released.load(); + mutex.unlock(); + holder.join(); + Require(afterRelease, "a spin that outlasted its window did not wait for the holder"); +} + +void CheckRecursion() { + std::recursive_mutex mutex; + LockSpinning(mutex, 100us); + LockSpinning(mutex, 100us); + mutex.unlock(); + mutex.unlock(); +} + +} + +int main() { + try { + CheckWindowPolicy(); + CheckUsableProcessors(); + CheckSpinUntil(); + Contend(0ns, "plain lock"); + Contend(50us, "spin 50 us"); + Contend(1ms, "spin 1 ms"); + Contend(150us, "recursive mutex"); + CheckBlockingFallback(); + CheckRecursion(); + std::cout << "handoff spin 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/HostImportRetireHold.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp index d1ce27fe58..92c6fe639a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -247,6 +247,41 @@ struct ActiveRecorder { } }; +void WindowPromotionOutlivesTheHold() { + struct RestoreWindow { + ~RestoreWindow() { SetHostImportRangeWindow(std::uint64_t{64} << 20u); } + } restore; + SetHostImportRangeWindow(Alignment); + Heap heap; + ActiveRecorder active; + auto& recorder = *active.recorder; + const auto context = mockContext(); + VkBuffer retired = VK_NULL_HANDLE; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* taken = HostImportFor(context, heap.Address(), 4096); + Expect(taken != nullptr && taken->bytes == Alignment, "the first access did not import one window"); + if (taken == nullptr) return; + retired = taken->buffer; + const auto* whole = HostImportFor(context, heap.Address(), RangeBytes); + Expect(whole != nullptr && whole->base == heap.Address() && whole->bytes == RangeBytes && whole->buffer != retired, "a held import window could not be promoted to its whole registered range"); + if (whole == nullptr) return; + Expect(Live(retired) && taken->buffer == retired && taken->bytes == Alignment, "promotion invalidated the held window or its entry"); + recorder.Commands(); + recorder.Submit(); + recorder.Sync(); + Expect(Live(retired), "a sync inside the promoting hold destroyed its old window"); + recorder.Commands(); + recorder.Submit(); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Expect(Live(retired), "the old window died while its hold's later batch was in flight"); + recorder.Sync(); + } + Expect(!Live(retired), "the old window survived every batch of the promoting hold"); +} + void IdleRetireOutlivesTheHold() { Heap heap; ActiveRecorder active; @@ -334,6 +369,7 @@ int main() { setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); #endif try { + WindowPromotionOutlivesTheHold(); IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index a6d05d4dc7..204e3213f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -40,6 +40,7 @@ void Expect(bool condition, const std::string& what) { constexpr std::uint64_t Alignment = 65536; constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; +constexpr std::uint64_t LargestTestedLimit = std::uint64_t{1} << 30u; struct Bind { VkDeviceSize resourceOffset; @@ -214,6 +215,10 @@ struct SharedRange { mutation.Add(backing.base, static_cast(bytes), true, true, true); } + ~SharedRange() { + std::erase(backings, &backing); + } + std::uint64_t Address() const { return reinterpret_cast(backing.base); } @@ -286,7 +291,12 @@ int main() { return 77; } close(udmabuf); + if (UdmabufLimitBytes() > LargestTestedLimit) { + std::printf("skipped, udmabuf size_limit_mb is above %llu: a range above it is too large to map\n", static_cast(LargestTestedLimit >> 20u)); + return 77; + } setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); + SetHostImportBudget(~std::uint64_t{0}); GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); try { ImportsALargeRangeAsOneSparseBuffer(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index ac2fff1057..1e59006cd6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "Recompiler.hpp" +#include "MeshGeometryFixture.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -12,6 +13,7 @@ #include #include #include +#include #include namespace { @@ -24,21 +26,7 @@ constexpr std::uint32_t Height = 128; constexpr std::array Background{16, 24, 40, 255}; alignas(256) std::array Pixels{}; -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, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, - 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, -}; +constexpr auto& GeometryCode = AgcDriver::Test::MeshGeometryCode; alignas(256) constexpr auto Wave32GeometryCode = [] { auto code = GeometryCode; @@ -54,6 +42,13 @@ alignas(256) constexpr std::array FastLaunchCode{ 0xbf810000, }; +alignas(256) constexpr std::array LineStripCode{ + 0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010, + 0x80020601, 0xb07c2003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302, + 0xf800020f, 0x09080706, 0x7d880082, 0xbefe036a, 0x161400ff, 0x00000401, 0x4a1414ff, 0x00000400, + 0xf8000941, 0x0000000a, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -98,6 +93,13 @@ alignas(256) std::array Indices{}; alignas(256) std::array Ordered{}; alignas(256) std::array Strip{}; +constexpr std::uint32_t LineStrips = 4; +alignas(256) std::array LineStripVertices{}; + +std::uint32_t LineStripRow(std::uint32_t strip) { + return 16u + 28u * strip; +} + constexpr std::uint32_t FanRim = 8; constexpr float FanStep = 6.28318530718f / FanRim; alignas(256) std::array Fan{}; @@ -292,6 +294,17 @@ int main() { const float y = i % 2u == 0u ? -1.0f : 0.0f; Strip[i] = {{x, y, 0.5f, 1.0f}, stripColor}; } + for (std::uint32_t strip = 0; strip < LineStrips; ++strip) { + const auto color = TriangleColor(strip); + const std::array rgba{color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, 1.0f}; + const float top = 1.0f - (static_cast(LineStripRow(strip)) + 0.5f) / (Height / 2.0f); + const float bottom = top - 20.0f / (Height / 2.0f); + const float left = 24.5f / (Width / 2.0f) - 1.0f; + const float right = 168.5f / (Width / 2.0f) - 1.0f; + LineStripVertices[3 * strip] = {{left, top, 0.5f, 1.0f}, rgba}; + LineStripVertices[3 * strip + 1] = {{right, top, 0.5f, 1.0f}, rgba}; + LineStripVertices[3 * strip + 2] = {{right, bottom, 0.5f, 1.0f}, rgba}; + } Fan[0] = {{0.0f, 0.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}; for (std::uint32_t k = 1; k <= FanRim; ++k) { const auto color = TriangleColor(k); @@ -373,6 +386,24 @@ int main() { } Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn"); + ClearPixels(); + auto lineSubgroup = FastLaunchSubgroup; + lineSubgroup.maxPrimitives = 2u; + lineSubgroup.outputPrimitiveSize = 2u; + DrawMesh(device, {lineSubgroup, {0, LineStrips, 0, 1, 0, false}, VertexBufferDescriptor(LineStripVertices.data(), static_cast(LineStripVertices.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, LineStripCode, 0x482030u}); + const auto linePixel = [](std::uint32_t x, std::uint32_t y) { return (static_cast(y) * Width + x) * 4u; }; + for (std::uint32_t strip = 0; strip < LineStrips; ++strip) { + const auto row = LineStripRow(strip); + const auto color = TriangleColor(strip); + const auto where = "line strip " + std::to_string(strip) + ": "; + for (std::uint32_t x = 32; x <= 160; x += 8) Require(PixelIs(linePixel(x, row), color), where + "horizontal line pixel (" + std::to_string(x) + ", " + std::to_string(row) + ") is " + PixelText(linePixel(x, row))); + for (std::uint32_t y = row + 4u; y <= row + 16u; y += 4) Require(PixelIs(linePixel(168, y), color), where + "vertical line pixel (168, " + std::to_string(y) + ") is " + PixelText(linePixel(168, y))); + for (const auto& [x, y, what] : {std::tuple{96u, row - 3u, "above the horizontal line"}, std::tuple{96u, row + 3u, "below the horizontal line"}, std::tuple{150u, row + 5u, "inside the triangle of the strip's three vertices"}, std::tuple{96u, row + 10u, "on the edge closing the strip"}, std::tuple{96u, row + 24u, "between this strip and the next"}, std::tuple{8u, row, "left of the strip"}, std::tuple{184u, row + 10u, "right of the strip"}}) { + Require(PixelIs(linePixel(x, y), Background), where + "pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") " + what + " is " + PixelText(linePixel(x, y))); + } + } + Require(PixelIs(0, Background) && PixelIs(Pixels.size() - 4u, Background), "line strips: the corners changed"); + constexpr std::size_t recordBlockBytes = 65536; auto* record = static_cast(::operator new(recordBlockBytes, std::align_val_t{65536})); { diff --git a/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp new file mode 100644 index 0000000000..0c59ec020a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP + +#include +#include + +namespace AgcDriver::Test { + +alignas(256) inline constexpr std::array MeshGeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp new file mode 100644 index 0000000000..4b819211da --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp @@ -0,0 +1,133 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "AllocationFailure.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; +using AgcDriver::Tests::SetAllocationFailureCountdown; +using namespace std::chrono_literals; + +namespace { + +void Check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +void Schedule(VkDevice device, std::function work) { + OptimizedPipeline pipeline; + pipeline.device = device; + pipeline.relink = std::move(work); + pipeline.deferred = true; + pipeline.Schedule(); +} + +} + +int main(int argc, char** argv) { + Check(argc == 2, "expected wait, clear or stop"); + const std::string_view mode(argv[1]); + Check(mode == "wait" || mode == "clear" || mode == "stop", "unknown drain mode"); + const auto device = reinterpret_cast(std::uintptr_t{1}); + const auto otherDevice = reinterpret_cast(std::uintptr_t{2}); + std::atomic completed{0}; + Schedule(device, [&] { ++completed; }); + WaitForOptimizedPipelines(device); + Check(completed == 1, "the initial job did not run"); + + bool allocationFailed = false; + OptimizedPipeline fresh; + fresh.device = otherDevice; + fresh.relink = [&] { ++completed; }; + fresh.deferred = true; + SetAllocationFailureCountdown(0); + try { fresh.Schedule(); } catch (const std::bad_alloc&) { allocationFailed = true; } + SetAllocationFailureCountdown(-1); + Check(allocationFailed, "the new device allocation did not fail"); + WaitForOptimizedPipelines(otherDevice); + Check(completed == 1, "a rejected job ran"); + + std::mutex mutex; + std::condition_variable changed; + bool entered = false; + bool released = false; + Schedule(device, [&] { + std::unique_lock lock(mutex); + entered = true; + changed.notify_all(); + changed.wait(lock, [&] { return released; }); + }); + { + std::unique_lock lock(mutex); + Check(changed.wait_for(lock, 3s, [&] { return entered; }), "the worker did not start"); + } + + allocationFailed = false; + unsigned accepted = 0; + for (unsigned attempt = 0; attempt < 512 && !allocationFailed; ++attempt) { + OptimizedPipeline pipeline; + pipeline.device = device; + pipeline.relink = [&] { ++completed; }; + pipeline.deferred = true; + SetAllocationFailureCountdown(0); + try { + pipeline.Schedule(); + ++accepted; + } catch (const std::bad_alloc&) { + allocationFailed = true; + } + SetAllocationFailureCountdown(-1); + } + Check(allocationFailed, "the queued jobs never needed another allocation"); + Check(completed == 1, "queued jobs ran while the worker was held"); + { + std::lock_guard lock(mutex); + released = true; + } + changed.notify_all(); + + bool drained = false; + std::thread drain([&] { + if (mode == "stop") StopPipelineLibraryWorker(); + if (mode == "clear") ClearPipelineLibraries(device); + else WaitForOptimizedPipelines(device); + { + std::lock_guard lock(mutex); + drained = true; + } + changed.notify_all(); + }); + { + std::unique_lock lock(mutex); + Check(changed.wait_for(lock, 3s, [&] { return drained; }), "a failed queue allocation left an unfinished job"); + } + drain.join(); + if (mode != "stop") { + Check(completed == accepted + 1, "an accepted job was lost or ran twice"); + Schedule(device, [&] { ++completed; }); + Schedule(otherDevice, [&] { ++completed; }); + WaitForOptimizedPipelines(device); + WaitForOptimizedPipelines(otherDevice); + Check(completed == accepted + 3, "the queue did not recover after allocation failure"); + StopPipelineLibraryWorker(); + } + WaitForOptimizedPipelines(device); + ClearPipelineLibraries(device); + ClearPipelineLibraries(otherDevice); + Check(PipelineLibraryWorkers() == 0, "the pipeline worker did not stop"); + std::printf("%.*s: rejected new-device and queue-growth allocations; %u jobs accepted before rejection, queue drained\n", + static_cast(mode.size()), mode.data(), accepted); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 39131eb9ca..00359e947e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -3,16 +3,27 @@ #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/Execution/include/Driver/Queues/SubmissionRing.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/FailureReport.hpp" #include "prx/libc/include/Shutdown.hpp" +#include #include #include +#include #include #include +#include +#include #include +#include +#include #include #include +#include +#include #include #include #include @@ -21,6 +32,9 @@ #define NOMINMAX #endif #include +#else +#include +#include #endif extern "C" int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size); @@ -62,18 +76,22 @@ void testCatalog() { for (const auto& opcode : AgcDriver::Pm4::Opcodes) { check(values.insert(opcode.value).second, "duplicate PM4 opcode"); const auto packet = makePacket(opcode.value, {0}); - check(AgcDriver::Pm4::Name(packet[0]) == opcode.name, "opcode name mismatch"); + check(AgcDriver::Pm4::Name(packet[0]) == opcode.name && AgcDriver::Pm4::KnownName(packet[0]) == opcode.name, "opcode name mismatch"); const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]); if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str()); } check(values.size() == 56, "reference opcode catalog is incomplete"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known"); + check(AgcDriver::Pm4::KnownName(makePacket(0xff, {0})[0]).empty() && AgcDriver::Pm4::Name(makePacket(0xff, {0})[0]) == "UNKNOWN_0xff", "an unknown opcode is named"); const std::array, 11> custom{{ {5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"}, {12, "POP_MARKER"}, {20, "ACQUIRE_MEM_CUSTOM"}, {21, "WRITE_DATA_CUSTOM"}, {23, "FLIP"}, {24, "RELEASE_MEM_CUSTOM"}, {25, "DMA_DATA_CUSTOM"}, {26, "CONTEXT_STATE"} }}; - for (const auto& [id, name] : custom) check(AgcDriver::Pm4::Name(makePacket(0x10, {0}, id << 2)[0]) == name, "custom opcode name mismatch"); + for (const auto& [id, name] : custom) { + const auto header = makePacket(0x10, {0}, id << 2)[0]; + check(AgcDriver::Pm4::Name(header) == name && AgcDriver::Pm4::KnownName(header) == name, "custom opcode name mismatch"); + } } void testNopPad() { @@ -1237,6 +1255,370 @@ void testAsyncMemoryFailure() { expectFailure([] { LibcRunShutdown_nid_postfix(); }, "guest"); } +class CapturedStderr { +public: + CapturedStderr() { +#ifdef _WIN32 + path = std::filesystem::temp_directory_path() / ("aps5-pm4-ring-" + std::to_string(GetCurrentProcessId()) + ".txt"); + file = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + check(file != INVALID_HANDLE_VALUE, "cannot create the stderr capture file"); + previous = GetStdHandle(STD_ERROR_HANDLE); + check(SetStdHandle(STD_ERROR_HANDLE, file) != 0, "cannot redirect stderr"); +#else + path = std::filesystem::temp_directory_path() / ("aps5-pm4-ring-" + std::to_string(::getpid()) + ".txt"); + file = ::open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0600); + check(file >= 0, "cannot create the stderr capture file"); + std::fflush(stderr); + previous = ::dup(2); + check(previous >= 0 && ::dup2(file, 2) >= 0, "cannot redirect stderr"); +#endif + } + + ~CapturedStderr() { +#ifdef _WIN32 + SetStdHandle(STD_ERROR_HANDLE, previous); + CloseHandle(file); +#else + std::fflush(stderr); + ::dup2(previous, 2); + ::close(previous); + ::close(file); +#endif + std::error_code ignored; + std::filesystem::remove(path, ignored); + } + + CapturedStderr(const CapturedStderr&) = delete; + CapturedStderr& operator=(const CapturedStderr&) = delete; + + std::vector RingLines() const { + std::ifstream input(path, std::ios::binary); + std::vector lines; + for (std::string line; std::getline(input, line);) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.starts_with("[pm4 ring]")) lines.push_back(line); + } + return lines; + } + +private: + std::filesystem::path path; +#ifdef _WIN32 + HANDLE file = INVALID_HANDLE_VALUE; + HANDLE previous = nullptr; +#else + int file = -1; + int previous = -1; +#endif +}; + +std::vector recordedWrites(const std::vector& lines) { + std::vector writes; + std::string entry; + for (const auto& line : lines) { + if (line.starts_with("[pm4 ring] queue ") && line.find(" #") != std::string::npos) { + entry = line.substr(17, line.find(" serial") - 17); + } else if (line.find("WRITE_DATA") != std::string::npos) { + check(!entry.empty() && line.size() >= 8, "a WRITE_DATA line is not under a submission"); + writes.push_back(entry + " " + line.substr(line.size() - 8)); + entry.clear(); + } + } + return writes; +} + +void submitWrites(std::uint32_t queue, std::uint32_t first, std::uint32_t count) { + alignas(8) static std::uint32_t target = 0; + for (std::uint32_t value = first; value < first + count; ++value) { + auto words = writeWord(target, value); + submitWords(words, queue); + } + AgcDriverWaitIdle_nid_postfix(); +} + +void testSubmissionRing() { + submitWrites(0, 0x5a000001u, 6); + submitWrites(0x20, 0x5b000001u, 2); + std::vector lines; + { + CapturedStderr capture; + AgcDriver::DriverDetail::DumpSubmissionRings("ring test"); + lines = capture.RingLines(); + } + check(!lines.empty() && lines.front() == "[pm4 ring] ring test: 2 queue(s), up to 4 submissions each, oldest first", "the ring dump header is wrong"); + check(std::find(lines.begin(), lines.end(), "[pm4 ring] queue 0x0: 6 submissions recorded, last 4 kept") != lines.end(), "the ring dump does not count every submission of queue 0"); + check(std::find(lines.begin(), lines.end(), "[pm4 ring] queue 0x20: 2 submissions recorded, last 2 kept") != lines.end(), "the ring dump does not count every submission of queue 0x20"); + const std::vector expected{"0x0 #3 5a000003", "0x0 #4 5a000004", "0x0 #5 5a000005", "0x0 #6 5a000006", "0x20 #1 5b000001", "0x20 #2 5b000002"}; + check(recordedWrites(lines) == expected, "the ring dump does not hold exactly the last submissions of each queue in order"); +} + +void testEmptySubmissionRing() { + CapturedStderr capture; + AgcDriver::DriverDetail::DumpSubmissionRings("ring test"); + check(capture.RingLines() == std::vector{"[pm4 ring] ring test: no submissions recorded"}, "the dump of empty rings is not one line"); +} + +void testDisabledSubmissionRing() { + submitWrites(0, 0x5a000001u, 2); + CapturedStderr capture; + AgcDriver::DriverDetail::DumpSubmissionRings("ring test"); + check(capture.RingLines() == std::vector{"[pm4 ring] ring test: disabled (APS5_PM4_RING=0)"}, "the dump of disabled rings is not one line"); +} + +void expectSettingRejected(const char* text) { + alignas(8) static std::uint32_t target = 0; + auto words = writeWord(target, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }, text); + AgcDriverWaitIdle_nid_postfix(); + check(target == 0, "a submission rejected for its ring or watchdog setting was executed"); +} + +void testInvalidSubmissionRing() { + expectSettingRejected("APS5_PM4_RING must be a number"); +} + +void testInvalidStallWatchdog() { + expectSettingRejected("APS5_STALL_WATCHDOG_SECS must be a number"); +} + +void testSubmissionRingFailureReport() { + submitWrites(0, 0x5a000001u, 3); + std::vector lines; + { + CapturedStderr capture; + LibcRunFailureReports_nid_no_patch(); + lines = capture.RingLines(); + } + check(!lines.empty() && lines.front() == "[pm4 ring] failure report: 1 queue(s), up to 2 submissions each, oldest first", "the failure report does not dump the rings"); + check(recordedWrites(lines) == std::vector{"0x0 #2 5a000002", "0x0 #3 5a000003"}, "the failure report does not hold the last submissions in order"); +} + +#ifdef _WIN32 +std::atomic heapTrapThread{0}; +std::atomic heapTrapAllocations{0}; +decltype(&HeapAlloc) heapAllocOriginal = nullptr; +decltype(&HeapReAlloc) heapReAllocOriginal = nullptr; + +LPVOID WINAPI trappedHeapAlloc(HANDLE heap, DWORD flags, SIZE_T bytes) { + if (GetCurrentThreadId() == heapTrapThread.load()) heapTrapAllocations.fetch_add(1); + return heapAllocOriginal(heap, flags, bytes); +} + +LPVOID WINAPI trappedHeapReAlloc(HANDLE heap, DWORD flags, LPVOID block, SIZE_T bytes) { + if (GetCurrentThreadId() == heapTrapThread.load()) heapTrapAllocations.fetch_add(1); + return heapReAllocOriginal(heap, flags, block, bytes); +} + +void** importSlot(HMODULE module, const char* function) { + auto* base = reinterpret_cast(module); + const auto* headers = reinterpret_cast(base + reinterpret_cast(base)->e_lfanew); + const auto& directory = headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + for (auto* import = reinterpret_cast(base + directory.VirtualAddress); import->Name != 0; ++import) { + const auto* names = reinterpret_cast(base + import->OriginalFirstThunk); + auto* slots = reinterpret_cast(base + import->FirstThunk); + for (; names->u1.AddressOfData != 0; ++names, ++slots) { + if (IMAGE_SNAP_BY_ORDINAL(names->u1.Ordinal)) continue; + const auto* byName = reinterpret_cast(base + names->u1.AddressOfData); + if (std::strcmp(reinterpret_cast(byName->Name), function) == 0) return reinterpret_cast(&slots->u1.Function); + } + } + return nullptr; +} + +void replaceSlot(void** slot, void* value) { + DWORD protection = 0; + check(VirtualProtect(slot, sizeof(void*), PAGE_READWRITE, &protection) != 0, "cannot unprotect a ucrtbase.dll import"); + *slot = value; + DWORD ignored = 0; + check(VirtualProtect(slot, sizeof(void*), protection, &ignored) != 0, "cannot protect a ucrtbase.dll import"); +} + +class CrtHeapTrap { +public: + CrtHeapTrap() { + const HMODULE runtime = GetModuleHandleW(L"ucrtbase.dll"); + check(runtime != nullptr, "ucrtbase.dll is not loaded"); + allocSlot = importSlot(runtime, "HeapAlloc"); + reAllocSlot = importSlot(runtime, "HeapReAlloc"); + check(allocSlot != nullptr && reAllocSlot != nullptr, "ucrtbase.dll does not import HeapAlloc and HeapReAlloc"); + heapAllocOriginal = reinterpret_cast(*allocSlot); + heapReAllocOriginal = reinterpret_cast(*reAllocSlot); + replaceSlot(allocSlot, reinterpret_cast(trappedHeapAlloc)); + replaceSlot(reAllocSlot, reinterpret_cast(trappedHeapReAlloc)); + } + + ~CrtHeapTrap() { + replaceSlot(allocSlot, reinterpret_cast(heapAllocOriginal)); + replaceSlot(reAllocSlot, reinterpret_cast(heapReAllocOriginal)); + } + + CrtHeapTrap(const CrtHeapTrap&) = delete; + CrtHeapTrap& operator=(const CrtHeapTrap&) = delete; + + template + std::uint32_t Allocations(TAction action) { + heapTrapAllocations.store(0); + heapTrapThread.store(GetCurrentThreadId()); + action(); + heapTrapThread.store(0); + return heapTrapAllocations.load(); + } + +private: + void** allocSlot = nullptr; + void** reAllocSlot = nullptr; +}; + +void testSubmissionRingFailureReportHeap() { + alignas(8) static std::uint32_t target = 0; + auto words = joinPackets({makePacket(0x13, {0x40}), writeWord(target, 0x5c000001u)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + std::vector lines; + { + CrtHeapTrap trap; + check(trap.Allocations([&] { check(AgcDriver::Pm4::Name(words[0]) == "INDEX_BUFFER_SIZE", "INDEX_BUFFER_SIZE is misnamed"); }) != 0, "the heap trap does not see the allocation of a packet name"); + CapturedStderr capture; + const auto allocations = trap.Allocations([] { LibcRunFailureReports_nid_no_patch(); }); + lines = capture.RingLines(); + check(allocations == 0, "the failure report allocated from the C runtime heap"); + } + check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 INDEX_BUFFER_SIZE 00000040") != std::string::npos; }), "the failure report does not name INDEX_BUFFER_SIZE"); + check(recordedWrites(lines) == std::vector{"0x0 #1 5c000001"}, "the failure report does not hold the submission"); +} +#endif + +bool hasRingLine(const std::vector& lines, std::string_view prefix, std::string_view suffix) { + return std::any_of(lines.begin(), lines.end(), [&](const std::string& line) { return line.size() >= prefix.size() + suffix.size() && line.starts_with(prefix) && line.ends_with(suffix); }); +} + +void awaitRingLine(const CapturedStderr& capture, std::string_view prefix, std::string_view suffix, const char* reason) { + for (int waited = 0; waited < 1000 && !hasRingLine(capture.RingLines(), prefix, suffix); ++waited) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + check(hasRingLine(capture.RingLines(), prefix, suffix), reason); +} + +void checkOneStallDump(const std::vector& lines, std::uint32_t& done) { + check(std::atomic_ref(done).load() == 1, "the stalled submission did not finish after it was released"); + check(std::count_if(lines.begin(), lines.end(), [](const std::string& line) { return line.find("has not progressed") != std::string::npos; }) == 1, "a stalled waiter dumped the rings more than once"); +} + +std::string stallPrefix(const char* waiter, const void* address) { + char text[160]; + std::snprintf(text, sizeof(text), "[pm4 ring] queue 0x0 %s 0x%llx has not progressed", waiter, static_cast(reinterpret_cast(address))); + return text; +} + +void testStallWatchdog() { + alignas(8) static std::uint32_t gate = 0; + alignas(8) static std::uint32_t done = 0; + std::vector lines; + { + CapturedStderr capture; + auto words = joinPackets({makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), writeWord(done, 1)}); + submitWords(words); + awaitRingLine(capture, stallPrefix("WAIT_REG_MEM at", &gate), " for 1 s (APS5_STALL_WATCHDOG_SECS): 1 queue(s), up to 16 submissions each, oldest first", + "a WAIT_REG_MEM stalled past APS5_STALL_WATCHDOG_SECS did not dump the rings"); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + check(std::atomic_ref(done).load() == 0, "the stalled submission finished before its gate was opened"); + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + lines = capture.RingLines(); + } + checkOneStallDump(lines, done); + check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 WAIT_REG_MEM 00000013") != std::string::npos; }), "the stall dump does not decode the stalled submission"); +} + +void testStallWatchdogRewind() { + alignas(8) static std::uint32_t done = 0; + static std::vector words; + words = joinPackets({makePacket(0x59, {0}), writeWord(done, 1)}); + std::vector lines; + { + CapturedStderr capture; + submitWords(words); + awaitRingLine(capture, stallPrefix("REWIND at", &words[1]), " for 1 s (APS5_STALL_WATCHDOG_SECS): 1 queue(s), up to 16 submissions each, oldest first", + "a REWIND stalled past APS5_STALL_WATCHDOG_SECS did not dump the rings"); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + check(std::atomic_ref(done).load() == 0, "the tail of a REWIND ran before the title released it"); + std::atomic_ref(words[1]).store(0x80000000u); + AgcDriverWaitIdle_nid_postfix(); + lines = capture.RingLines(); + } + checkOneStallDump(lines, done); + check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 REWIND 00000000") != std::string::npos; }), "the stall dump does not decode the stalled REWIND"); +} + +struct RenderingGateState { + std::mutex mutex; + std::condition_variable changed; + bool closed = true; + bool waiting = false; + + void Open() { + { + std::lock_guard lock(mutex); + closed = false; + } + changed.notify_all(); + } +}; + +class RenderingGate final : public AgcDriver::IRenderingWait { +public: + explicit RenderingGate(std::shared_ptr state) : state(std::move(state)) {} + + void Wait() override { + std::unique_lock lock(state->mutex); + state->waiting = true; + state->changed.notify_all(); + state->changed.wait(lock, [&] { return !state->closed; }); + } + +private: + std::shared_ptr state; +}; + +class GatedOutput final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { throw std::runtime_error("the gated test output takes no flips"); } + std::shared_ptr CaptureRenderingWait(std::uint32_t) override { return std::make_shared(state); } + void Fail(std::exception_ptr) noexcept override {} + + std::shared_ptr state = std::make_shared(); +}; + +void testStallWatchdogOrder() { + alignas(8) static std::uint32_t done = 0; + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + struct GateRelease { + std::shared_ptr state; + ~GateRelease() { state->Open(); } + } release{output->state}; + std::vector lines; + { + CapturedStderr capture; + std::vector wait{AgcDriver::RenderingWaitPacketHeader, 7, 0, 0}; + submitWords(wait); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "queue 0 did not reach the rendering wait"); + } + auto words = writeWord(done, 1); + submitWords(words, 0x20); + awaitRingLine(capture, "[pm4 ring] queue 0x20 order behind queue 0 for event 0x", " has not progressed for 1 s (APS5_STALL_WATCHDOG_SECS): 2 queue(s), up to 16 submissions each, oldest first", + "a compute submission held behind queue 0 past APS5_STALL_WATCHDOG_SECS did not dump the rings"); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + check(std::atomic_ref(done).load() == 0, "a compute submission passed queue 0 while it was held"); + output->state->Open(); + AgcDriverWaitIdle_nid_postfix(); + lines = capture.RingLines(); + } + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); + checkOneStallDump(lines, done); +} + void testRefusedWorkerPriority() { alignas(8) static std::uint32_t result = 0; auto words = writeWord(result, 91); @@ -1268,6 +1650,28 @@ int main(int argc, char** argv) { std::puts("PM4 refused queue worker priority propagated to idle and shutdown"); return 0; } + const std::pair ringModes[]{ + {"ring", testSubmissionRing}, {"ring-empty", testEmptySubmissionRing}, {"ring-disabled", testDisabledSubmissionRing}, + {"ring-invalid", testInvalidSubmissionRing}, {"ring-report", testSubmissionRingFailureReport}, {"stall-watchdog", testStallWatchdog}, + {"stall-watchdog-invalid", testInvalidStallWatchdog}, {"stall-watchdog-rewind", testStallWatchdogRewind}, {"stall-watchdog-order", testStallWatchdogOrder}}; + if (argc == 2 && std::string(argv[1]) == "ring-report-heap") { +#ifdef _WIN32 + testSubmissionRingFailureReportHeap(); + LibcRunShutdown_nid_postfix(); + std::puts("PM4 ring-report-heap test passed"); + return 0; +#else + std::puts("the C runtime heap trap needs ucrtbase.dll"); + return 77; +#endif + } + for (const auto& [mode, test] : ringModes) { + if (argc != 2 || std::string(argv[1]) != mode) continue; + test(); + LibcRunShutdown_nid_postfix(); + std::printf("PM4 %s test passed\n", mode); + return 0; + } testCatalog(); testNopPad(); testWriteChangedKeepsUntouchedBytes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..580996f921 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +109,63 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + devices[0].apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, "AMD").has_value(), "a requested device without Vulkan 1.1 was selected"); + Require(selectDevice(devices, {}) == 1u, "an unsupported device changed default selection"); + devices[0] = devices[1]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "NVIDIA") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); +} + // A compute-capable device with host imports (VK_EXT_external_memory_host) when the host offers // them, as the driver creates its own; the recorder's batches need a real queue and real fences. class Device { @@ -132,28 +191,14 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - const auto rankDeviceType = [](VkPhysicalDeviceType type) { - switch (type) { - case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; - case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; - case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; - default: return 0; - } - }; const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); - int selectedRank = -1; - for (const auto physical : devices) { - VkPhysicalDeviceProperties candidate{}; - physicalProperties(physical, &candidate); - if (candidate.apiVersion < VK_API_VERSION_1_1) continue; - const int rank = rankDeviceType(candidate.deviceType); - if (rank <= selectedRank) continue; - context.physical = physical; - selectedRank = rank; - } - Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); - VkPhysicalDeviceProperties selected{}; - physicalProperties(context.physical, &selected); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; + const auto& selected = deviceProperties[*selectedIndex]; std::cout << "Vulkan device: " << selected.deviceName << '\n'; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); @@ -210,6 +255,8 @@ public: context.occlusionQueryPrecise = supported.occlusionQueryPrecise == VK_TRUE; enabled.shaderStorageImageReadWithoutFormat = supported.shaderStorageImageReadWithoutFormat; enabled.shaderStorageImageWriteWithoutFormat = supported.shaderStorageImageWriteWithoutFormat; + enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; + context.storageImageMultisample = supported.shaderStorageImageMultisample == VK_TRUE; context.singlePassStorage = supported.shaderStorageImageReadWithoutFormat == VK_TRUE && supported.shaderStorageImageWriteWithoutFormat == VK_TRUE; address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; @@ -1980,10 +2027,12 @@ void resourceReadTests(const Device& device, Recorder& recorder) { auto first = resources.PrepareDrawBindings(snapshotRecorder); Require(first != nullptr && first->snapshots.size() == 1, "read-only draw input was not snapshotted"); std::memset(reinterpret_cast(element), 0x22, elementBytes); + AgcDriver::GuestMemory::MarkWritten(element, elementBytes); auto second = resources.PrepareDrawBindings(snapshotRecorder); Require(second != nullptr && second->snapshots.size() == 1, "cached draw input was not snapshotted again"); Require(first->allocation.set != second->allocation.set, "in-flight draws share mutable descriptor bindings"); std::memset(reinterpret_cast(element), 0x33, elementBytes); + AgcDriver::GuestMemory::MarkWritten(element, elementBytes); auto downloaded = std::make_shared(snapshotContext, elementBytes * 2, VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = snapshotRecorder.Commands(); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); @@ -2341,6 +2390,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { }; check(); guest[offset + 8] ^= 0xffu; + AgcDriver::GuestMemory::MarkWritten(view, elementBytes); check(); regionRecorder.Sync(); } @@ -3131,10 +3181,16 @@ void drawInputInPlaceTests(const Device& device, Recorder& recorder) { Require(InPlaceDrawInput(context, vertices + 2, 256, 2) != nullptr, "an aligned 16-bit draw input was not bound in place"); recorder.NotePendingWrite(address + 8192, 16); Require(InPlaceDrawInput(context, address + 8192, 256, 4) != nullptr && !recorder.PendingWriteOverlaps(address + 8192, 256), "a draw input under a pending GPU write was bound before the write completed"); + bool behind = false; + Require(InPlaceDrawInput(context, address + 20480, 256, 4, &behind) != nullptr && !behind, "a vertex input free of pending GPU writes was marked as ordered behind one"); + recorder.NotePendingWrite(address + 20480, 16); + Require(InPlaceDrawInput(context, address + 20480, 256, 4, &behind) != nullptr && behind && recorder.PendingWriteOverlaps(address + 20480, 256), "a vertex input under a pending GPU write was not bound in place behind it"); const std::array label{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; recorder.RecordStore(import->buffer, address + 12288 - import->base, label, address + 12288); Require(recorder.QueuedStoreOverlaps(address + 12288, 4), "the label store was not queued"); Require(InPlaceDrawInput(context, address + 12288, 256, 4) == nullptr, "a draw input under a queued label store was bound in place"); + behind = false; + Require(InPlaceDrawInput(context, address + 12288, 256, 4, &behind) == nullptr, "a vertex input under a queued label store was bound in place"); Require(InPlaceDrawInput(context, address + 16384, 256, 4) != nullptr, "a queued label store refused a disjoint draw input"); alignas(64) static std::array unregistered{}; Require(InPlaceDrawInput(context, reinterpret_cast(unregistered.data()), unregistered.size(), 4) == nullptr, "memory without a host import was bound in place"); @@ -3801,6 +3857,11 @@ void importWatchTests(const Device& device) { std::cout << "host imports or write watching unavailable: the Windows import probe not tested\n"; return; } + if (context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr) { + Require(PrepareImportWatch(context) == ImportWatch::Unwatch, "(w) a device without a settle queue kept host imports watched"); + std::cout << "no settle queue: the Windows import probe skipped\n"; + return; + } const auto probe = ProbeImportWriteProtection(context); Require(probe.failure == nullptr, std::string("(w) the Windows import probe failed at ") + (probe.failure != nullptr ? probe.failure : "") + " (" + std::to_string(static_cast(probe.result)) + ")"); std::cout << "Windows import probe: " << probe.writtenAtImport << " of " << probe.pages << " scratch pages reported written at the import, " << probe.writtenDuringSettle << " by its 4-byte settle read, " << probe.writtenAfterSettle << " after it, " << probe.writtenAfterSubmit << " after a full GPU read, " << probe.writtenAfterSecondSubmit << " after a second, " << probe.writtenAfterFill << " after a GPU write, " << probe.writtenByCpu << " after a CPU store\n"; @@ -5325,35 +5386,37 @@ class CoverPass { public: explicit CoverPass(const Context& context) : context(context), target(context, ColorTarget{0, {8, 8}, VK_FORMAT_R8G8B8A8_UNORM, 8 * 8 * 4, 0}, false) { try { - VkAttachmentDescription attachment{}; - attachment.format = VK_FORMAT_R8G8B8A8_UNORM; - attachment.samples = VK_SAMPLE_COUNT_1_BIT; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; - const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color; - VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; - passInfo.attachmentCount = 1; - passInfo.pAttachments = &attachment; - passInfo.subpassCount = 1; - passInfo.pSubpasses = &subpass; - Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass cover"); - const auto view = target.View(); - VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; - framebufferInfo.renderPass = renderPass; - framebufferInfo.attachmentCount = 1; - framebufferInfo.pAttachments = &view; - framebufferInfo.width = 8; - framebufferInfo.height = 8; - framebufferInfo.layers = 1; - Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer cover"); + if (!context.graphicsPipelineLibrary) { + VkAttachmentDescription attachment{}; + attachment.format = VK_FORMAT_R8G8B8A8_UNORM; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass cover"); + const auto view = target.View(); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = 8; + framebufferInfo.height = 8; + framebufferInfo.layers = 1; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer cover"); + } VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout cover"); const auto module = [&](const std::uint32_t* code, std::size_t bytes) { @@ -5400,7 +5463,12 @@ public: pipelineInfo.pMultisampleState = &multisample; pipelineInfo.pColorBlendState = &blendState; pipelineInfo.layout = layout; - pipelineInfo.renderPass = renderPass; + const VkFormat colorFormat = VK_FORMAT_R8G8B8A8_UNORM; + VkPipelineRenderingCreateInfoKHR renderingFormats{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; + renderingFormats.colorAttachmentCount = 1; + renderingFormats.pColorAttachmentFormats = &colorFormat; + if (context.graphicsPipelineLibrary) pipelineInfo.pNext = &renderingFormats; + else pipelineInfo.renderPass = renderPass; Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines cover"); } catch (...) { release(); @@ -5417,11 +5485,35 @@ public: const auto commands = start.commands; if (!continued) { recorder.PrepareSampleSlot(); - VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = renderPass; - begin.framebuffer = framebuffer; - begin.renderArea = {{0, 0}, {8, 8}}; - context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + if (context.graphicsPipelineLibrary) { + if (!transitioned) { + transitioned = true; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.image = target.Image(); + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + VkRenderingAttachmentInfoKHR attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; + attachment.imageView = target.View(); + attachment.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; + rendering.renderArea = {{0, 0}, {8, 8}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); + } else { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = renderPass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, {8, 8}}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + } } context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); recorder.NoteSampledDraw(commands); @@ -5450,6 +5542,7 @@ private: } const Context& context; RenderTarget target; + mutable bool transitioned = false; VkRenderPass renderPass = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; @@ -5550,7 +5643,7 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { return; } recorder.Sync(); - std::memset(keys, 0x00, keyCount); + std::memset(keys, 0x00, bytes); MarkDccUncompressed(context, address, surfaceBytes); Require(recorder.PendingWriteOverlaps(address, keyCount) && keys[0] == 0x00, "the uncompressed key store did not stay pending"); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed, "the keys after a pending uncompressed store do not read as uncompressed"); @@ -6228,6 +6321,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { { std::lock_guard gpu(GpuMutex()); Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + Require(!MultisampleTargetStorable(context, target), "a multisampled target nothing stored to was created with storage usage"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -6313,7 +6407,20 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 3u, address), "as a 64x64 texture of 8 samples", "sampling a 4x target as an 8x texture"); refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side / 2u, 2u, address), "as a 32x64 texture of 4 samples", "sampling a 4x target as a narrower texture"); refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address + 4u * 65536u), "which no draw rendered to and whose tile mode 0 has no modeled multisampled color footprint", "sampling a linear multisampled texture no draw rendered to"); - refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "storage image access to a multisampled texture", "storage access to a multisampled texture"); + if (context.storageImageMultisample) { + const auto stored = sampledProgram(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address)); + const CompiledShader storedCompute{ShaderRecompiler::ShaderStage::Compute, &stored, 0}; + std::lock_guard gpu(GpuMutex()); + const auto rendered = MultisampleTargetView(context, target); + Require(!MultisampleTargetStorable(context, target), "sampling the multisampled target gave it storage usage"); + ShaderResources resources(context, storedCompute); + Require(MultisampleTargetStorable(context, target) && MultisampleTargetView(context, target) != rendered, "a storage binding did not move the multisampled target to an image with storage usage"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } else { + refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "as a storage image at its sample count", "storage access to a multisampled target without shaderStorageImageMultisample"); + } 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); clearTargets(); @@ -6424,7 +6531,7 @@ void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else - void* block = std::aligned_alloc(65536, bytes); + void* block = std::aligned_alloc(65536, (bytes + 65535u) & ~std::size_t{65535u}); #endif Require(block != nullptr, "cannot allocate the FMASK decompress block"); std::memset(block, 0, bytes); @@ -6890,6 +6997,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +struct SampledViewProbe { + Context& context; + PFN_vkGetDeviceProcAddr resolver; + std::uint32_t views = 0; + bool checking = false; + static inline SampledViewProbe* active = nullptr; + explicit SampledViewProbe(Context& context) : context(context), resolver(context.deviceProc) { + Require(active == nullptr, "sampled view probe is already active"); + active = this; + context.deviceProc = resolve; + } + ~SampledViewProbe() { + context.deviceProc = resolver; + active = nullptr; + } + static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) { + if (active->checking && info->format == VK_FORMAT_R8G8B8A8_SRGB) { + const auto* next = static_cast(info->pNext); + while (next != nullptr && next->sType != VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO) next = next->pNext; + if (next == nullptr || reinterpret_cast(next)->usage != VK_IMAGE_USAGE_SAMPLED_BIT) return VK_ERROR_VALIDATION_FAILED_EXT; + ++active->views; + } + return reinterpret_cast(active->resolver(device, "vkCreateImageView"))(device, info, callbacks, created); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast(createView); + return active->resolver(device, name); + } +}; + +void sampledStorageBaseLevelUsageTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = resource.height = 8; + resource.depthOrLastArray = 1; + resource.mipCount = 2; + resource.lastLevel = 1; + resource.minLod = context.imageViewMinLod ? 0x100 : 0; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2DArray; + resource.format = 130; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + std::vector memory(static_cast(geometry.guestBytes) + 256, std::byte{0x80}); + resource.baseAddress = (reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}; + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + recorder.Sync(); + SampleProgram program(context, recorder); + auto sampledContext = context; + SampledViewProbe probe(sampledContext); + Texture sampled(sampledContext, storage, resource, VkComponentMapping{}); + probe.checking = true; + const auto base = sampled.BaseLevelView(); + probe.checking = false; + expectRed(program.Red(base, sampled.Layout(), 0), 0.21586f, "sampled sRGB base level"); + Require(sampled.BaseLevelView() == base && probe.views == 1, "the lazy base-level view of a sampled storage image must restrict usage to SAMPLED and be reused"); +} + void firstLayerViewTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); TextureDetiler detiler(context); @@ -7205,7 +7376,7 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor 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"); + Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput || read->kind == Recorder::ReadKind::StorageUpload), "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); @@ -7308,10 +7479,13 @@ void pipelineCacheTests(const Device& device, bool benchmark) { input = {}; Require(lookup(input) == first, "pipeline cache retained bytes from a larger key"); vertex.variantId = 0; - const auto unidentified = lookup(input); - Require(lookup(input) != unidentified, "pipeline cache retained an unidentified shader"); + Require(lookup(input) == first, "pipeline cache keyed on the variant id instead of the module"); + vertex.spirv[2] ^= 1u; + const auto other = lookup(input); + Require(other != first && lookup(input) == other, "pipeline cache ignored another module"); + vertex.spirv[2] ^= 1u; vertex.variantId = 1; - Require(lookup(input) == first, "an unidentified shader disturbed the retained pipeline"); + Require(lookup(input) == first, "another module disturbed the retained pipeline"); ClearCachedPipelines(context.device); Require(lookup(input) != first, "clearing the pipeline cache retained its old entry"); std::cout << "Pipeline cache key and retained lifetime tests passed\n"; @@ -7429,6 +7603,43 @@ int pipelineCacheFileTests(const Device& device) { PipelineCache idle(context, properties); } Require(contents(path) == stored, "an object that added no pipelines rewrote the cache file"); + { + PipelineCache unseeded(context, properties, false); + Require(!holds(unseeded.Handle(), 0), "a cache created without the file already held a pipeline of the file"); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "an unseeded cache dropped pipelines from the cache file when it saved"); + } + std::filesystem::remove_all(directory); + { + PipelineCache writer(context, properties); + build(writer.Handle(), 0); + } + { + PipelineCache unseeded(context, properties, false); + unseeded.SaveNow(); + build(unseeded.Handle(), 1); + unseeded.SaveNow(); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "an unseeded cache that saved twice over an unchanged file lost pipelines of the file"); + } + std::filesystem::remove_all(directory); + { + PipelineCache writer(context, properties); + build(writer.Handle(), 0); + } + { + PipelineCache extender(context, properties); + Require(holds(extender.Handle(), 0), "the cache file lost a pipeline"); + build(extender.Handle(), 1); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "a cache that saved over an unchanged file lost pipelines"); + } { std::ofstream corrupt(path, std::ios::binary | std::ios::trunc); corrupt << "not a pipeline cache"; @@ -7746,13 +7957,13 @@ void pipelineLibraryTests(const Device& device) { Require(StagePushOffset(88, ShaderRecompiler::ShaderStage::Fragment, true) == PipelinePushSlotBytes && StagePushOffset(88, ShaderRecompiler::ShaderStage::Fragment, false) == 88 && StagePushOffset(40, ShaderRecompiler::ShaderStage::TessellationEvaluation, true) == 40, "a stage got the wrong push offset"); const auto beforeSlots = counters().built; ShaderRecompiler::RecompileResult otherVertex = vertex; - otherVertex.variantId = 14; + otherVertex.spirv[2] ^= 1u; std::array slotted{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}; static_cast(CachedPipeline(context, state, input, resources, slotted, VK_IMAGE_LAYOUT_GENERAL)); slotted.front().program = &otherVertex; static_cast(CachedPipeline(context, state, input, resources, slotted, VK_IMAGE_LAYOUT_GENERAL)); Require(counters().built[2] == beforeSlots[2] + 1u && counters().built[1] == beforeSlots[1] + 1u, "a pixel shader in its fixed slot built its library again for another vertex shader"); - fragment.variantId = 13; + fragment.spirv[2] ^= 1u; static_cast(lookup()); Require(counters().built == std::array{2, 2, 3, 2}, "a new pixel shader rebuilt more than the fragment shader library"); state.colors.front().format = VK_FORMAT_R16G16B16A16_SFLOAT; @@ -7965,6 +8176,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { + deviceSelectionTests(); + std::cout << "recorder device selection tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--queued-write-groups-only") { queuedWriteGroupTests(); std::cout << "queued write group tests passed\n"; @@ -8077,6 +8293,10 @@ int main(int argc, char** argv) { std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sampled-storage-base-level-usage-only") { + sampledStorageBaseLevelUsageTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unchanged-cpu-stamps-only") { unchangedCpuStampTests(device, recorder); return 0; @@ -8294,6 +8514,7 @@ int main(int argc, char** argv) { imageMemoTests(device, recorder); minLodTests(device, recorder); firstLayerViewTests(device, recorder); + sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp index 4997d6be4d..1158bc94a3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include #include #include @@ -30,16 +31,27 @@ struct MockDevice { std::map signaled; std::uint64_t submits = 0; std::uint64_t fenceWaits = 0; + std::uint64_t allocatedCommands = 0; + std::uint64_t freedCommands = 0; + VkResult endResult = VK_SUCCESS; + VkResult submitResult = VK_SUCCESS; + bool lostSubmissionPending = false; + std::uint64_t idleWaits = 0; + VkResult idleResult = VK_ERROR_DEVICE_LOST; }; MockDevice mock; VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { *commands = reinterpret_cast(mock.next++); + ++mock.allocatedCommands; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t, const VkCommandBuffer*) {} +VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t count, const VkCommandBuffer*) { + Expect(!mock.lostSubmissionPending, "command buffers were freed before the lost submission finished"); + mock.freedCommands += count; +} VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = reinterpret_cast(mock.next++); @@ -48,6 +60,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo } VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) { + Expect(!mock.lostSubmissionPending, "a fence was destroyed before the lost submission finished"); mock.signaled.erase(fence); } @@ -62,6 +75,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t count, } VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t count, const VkFence* fences) { + Expect(!mock.lostSubmissionPending, "a fence was reset before the lost submission finished"); for (std::uint32_t i = 0; i < count; ++i) mock.signaled.at(fences[i]) = false; return VK_SUCCESS; } @@ -71,12 +85,19 @@ VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkC } VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { - return VK_SUCCESS; + return mock.endResult; } VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { ++mock.submits; - return VK_SUCCESS; + if (mock.submitResult == VK_ERROR_DEVICE_LOST) mock.lostSubmissionPending = true; + return mock.submitResult; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockDeviceWaitIdle(VkDevice) { + ++mock.idleWaits; + mock.lostSubmissionPending = false; + return mock.idleResult; } VKAPI_ATTR void VKAPI_CALL mockCmdUpdateBuffer(VkCommandBuffer, VkBuffer, VkDeviceSize, VkDeviceSize, const void*) {} @@ -101,6 +122,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, + {"vkDeviceWaitIdle", reinterpret_cast(mockDeviceWaitIdle)}, {"vkCmdUpdateBuffer", reinterpret_cast(mockCmdUpdateBuffer)}, {"vkCmdPipelineBarrier", reinterpret_cast(mockCmdPipelineBarrier)}, {"vkCmdBeginQuery", reinterpret_cast(mockCmdBeginQuery)}, @@ -216,6 +238,68 @@ void SharedOwnerCountsOncePerBatch() { recorder.Sync(); } +void FailedSubmission(VkResult endResult, VkResult submitResult, VkResult idleResult = VK_ERROR_DEVICE_LOST) { + mock = MockDevice{}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + alignas(8) std::array memory{}; + const auto address = reinterpret_cast(memory.data()); + bool completed = false; + bool earlierCompleted = false; + bool earlierWrittenBack = false; + { + Recorder recorder(mockContext()); + recorder.Activate(); + recorder.Keep(std::make_shared(0), 17); + recorder.NoteLabel(address, std::span(memory).first(4), 1, 0); + recorder.OnComplete([&] { earlierWrittenBack = true; }); + Expect(recorder.AfterRecordedWork([&] { earlierCompleted = true; }), "the earlier batch did not accept its completion"); + recorder.Submit(); + auto kept = std::shared_ptr(new int(1), [](int* object) { + Expect(!mock.lostSubmissionPending, "kept resources were destroyed before the lost submission finished"); + delete object; + }); + std::weak_ptr released = kept; + recorder.Keep(std::move(kept), Budget); + recorder.NoteLabel(address + 8, std::span(memory).first(4), 2, 0); + recorder.NoteLabel(address + 16, std::span(memory).first(Recorder::LabelTableBytes + 4), 3, 0); + recorder.AfterCompletions(address + 96, std::span(memory).first(4), 4, 0, false); + recorder.OnComplete([&] { completed = true; }); + Expect(recorder.AfterRecordedWork([&] { completed = true; }), "the failed batch did not accept its completion"); + mock.endResult = endResult; + mock.submitResult = submitResult; + mock.idleResult = idleResult; + bool failed = false; + try { + recorder.Submit(); + } catch (const std::exception& error) { + const auto operation = endResult != VK_SUCCESS ? "vkEndCommandBuffer recorder" : "vkQueueSubmit recorder"; + failed = std::string_view(error.what()).find(operation) != std::string_view::npos; + } + Expect(failed, "submission did not report its Vulkan failure"); + Expect(released.expired() && !completed, "a failed batch retained objects or ran its completions"); + Expect(!recorder.Recording() && recorder.Submissions() == 1 && recorder.InFlightBatches() == 1 && recorder.InFlightKeptBytes() == 17, + "the failed batch changed the earlier in-flight submission"); + Expect(recorder.PendingLabels() == 1 && recorder.PendingLabelIn(address, 4), "a failed batch retained labels or removed the earlier batch's label"); + Expect(!recorder.PendingLabelIn(address + 8, 4) && !recorder.PendingLabelIn(address + 16, Recorder::LabelTableBytes + 4), + "a failed batch retained a narrow or wide label"); + Expect(!recorder.CompletionLabelIn(address + 96, 4) && Recorder::PendingCompletionLabels() == 1 && Recorder::PendingWriteBackCompletions() == 1, + "a failed batch retained pending completions or removed earlier ones"); + Expect(!Recorder::PendingLabelSince().has_value(), "a failed batch left its label flush deadline pending"); + Expect(Recorder::SnapshotWriteOverlaps(address, 4) && !Recorder::SnapshotWriteOverlaps(address + 8, 100), + "a failed batch left stale writes in the pending snapshot or removed an earlier write"); + Expect(mock.fenceWaits == 0, "a failed submission waited for an unsignaled fence"); + Expect(mock.idleWaits == (submitResult == VK_ERROR_DEVICE_LOST ? 1u : 0u), "a failed submission did not settle device loss before releasing its resources"); + mock.lostSubmissionPending = false; + mock.endResult = mock.submitResult = VK_SUCCESS; + recorder.Sync(); + Expect(recorder.Idle() && !completed && earlierCompleted && earlierWrittenBack, + "sync ran the failed batch's completions or lost the earlier batch's completions"); + Expect(Recorder::PendingCompletionLabels() == 0 && Recorder::PendingWriteBackCompletions() == 0, + "sync left stale completion counts after submission failure"); + } + Expect(mock.signaled.empty() && mock.allocatedCommands == mock.freedCommands, "a failed submission leaked its fence or command buffer"); +} + } int main() { @@ -225,6 +309,12 @@ int main() { SyncedWithinTwiceTheBudget(); CountFollowsEveryPath(); SharedOwnerCountsOncePerBatch(); + FailedSubmission(VK_ERROR_OUT_OF_HOST_MEMORY, VK_SUCCESS); + FailedSubmission(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_SUCCESS); + FailedSubmission(VK_SUCCESS, VK_ERROR_OUT_OF_HOST_MEMORY); + FailedSubmission(VK_SUCCESS, VK_ERROR_OUT_OF_DEVICE_MEMORY); + FailedSubmission(VK_SUCCESS, VK_ERROR_DEVICE_LOST); + FailedSubmission(VK_SUCCESS, VK_ERROR_DEVICE_LOST, VK_SUCCESS); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 1143e50b0d..20112c62f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -16,8 +17,8 @@ using AgcDriver::Graphics::Require; struct ProfileInput { std::vector capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; - std::array extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - std::array extensions{extensionStrings[0], extensionStrings[1]}; + std::vector extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + std::vector extensions{extensionStrings[0], extensionStrings[1]}; VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, nullptr, VK_FALSE, VK_FALSE, VK_TRUE}; VkPhysicalDeviceDescriptorIndexingFeatures indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, &robustness}; VkPhysicalDevice8BitStorageFeatures bytes{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, &indexing, VK_TRUE}; @@ -54,7 +55,7 @@ void Reject(TAction action, const char* expected) { Require(std::string(error.what()).find(expected) != std::string::npos, "unexpected profile validation error"); return; } - throw std::runtime_error("invalid shader device profile was accepted"); + throw std::runtime_error(std::string("invalid shader device profile was accepted: ") + expected); } void CheckProfile() { @@ -80,6 +81,7 @@ void CheckProfile() { invalid([](auto& value) { value.bda.bufferDeviceAddress = VK_FALSE; }, "bufferDeviceAddress"); invalid([](auto& value) { value.device.pNext = nullptr; }, "bufferDeviceAddress"); invalid([](auto& value) { value.core.shaderInt64 = VK_FALSE; }, "shaderInt64"); + invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityGeometry); }, "geometryShader"); invalid([](auto& value) { value.bytes.storageBuffer8BitAccess = VK_FALSE; }, "storageBuffer8BitAccess"); ProfileInput withoutNull; withoutNull.robustness.nullDescriptor = VK_FALSE; @@ -99,6 +101,8 @@ void CheckProfile() { invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityStorageImageMultisample); }, "shaderStorageImageMultisample"); invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); }, "fragmentShaderBarycentric"); invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityMeshShadingEXT); }, "meshShader"); + invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityDrawParameters); }, "shaderDrawParameters"); + invalid([](auto& value) { value.extensions.push_back("SPV_KHR_terminate_invocation"); }, "shaderTerminateInvocation"); ProfileInput enabled; enabled.capabilities.push_back(spv::CapabilitySampledImageArrayDynamicIndexing); enabled.capabilities.push_back(spv::CapabilityStorageImageArrayNonUniformIndexing); @@ -106,6 +110,28 @@ void CheckProfile() { enabled.indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; const AgcDriver::ShaderDeviceProfile accepted(enabled.Target(), enabled.device, enabled.limits); Require(accepted.Target().supportedCapabilities.size() == 6u, "enabled descriptor indexing was rejected"); + ProfileInput drawParameters; + drawParameters.capabilities.push_back(spv::CapabilityDrawParameters); + VkPhysicalDeviceShaderDrawParametersFeatures drawFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, &drawParameters.bda, VK_TRUE}; + drawParameters.device.pNext = &drawFeatures; + const AgcDriver::ShaderDeviceProfile drawParametersAccepted(drawParameters.Target(), drawParameters.device, drawParameters.limits); + Require(std::ranges::find(drawParametersAccepted.Target().supportedCapabilities, spv::CapabilityDrawParameters) != drawParametersAccepted.Target().supportedCapabilities.end(), "enabled shaderDrawParameters was rejected"); + ProfileInput terminating; + VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR terminate{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR, &terminating.bda, VK_TRUE}; + terminating.device.pNext = &terminate; + terminating.extensions.push_back("SPV_KHR_terminate_invocation"); + const AgcDriver::ShaderDeviceProfile terminated(terminating.Target(), terminating.device, terminating.limits); + Require(std::ranges::find(terminated.Target().supportedExtensions, "SPV_KHR_terminate_invocation") != terminated.Target().supportedExtensions.end(), "enabled shaderTerminateInvocation was rejected"); + ProfileInput disabled; + VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR disabledTerminate{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR, &disabled.bda, VK_FALSE}; + disabled.device.pNext = &disabledTerminate; + disabled.extensions.push_back("SPV_KHR_terminate_invocation"); + Reject([&] { AgcDriver::ShaderDeviceProfile rejected(disabled.Target(), disabled.device, disabled.limits); }, "shaderTerminateInvocation"); + ProfileInput geometryEnabled; + geometryEnabled.capabilities.push_back(spv::CapabilityGeometry); + geometryEnabled.core.geometryShader = VK_TRUE; + const AgcDriver::ShaderDeviceProfile geometryAccepted(geometryEnabled.Target(), geometryEnabled.device, geometryEnabled.limits); + Require(std::ranges::find(geometryAccepted.Target().supportedCapabilities, spv::CapabilityGeometry) != geometryAccepted.Target().supportedCapabilities.end(), "enabled geometry shaders were rejected"); } void CheckAbi() { @@ -142,6 +168,11 @@ void CheckHeaps() { const auto single = allocate(image, 1u); const auto sixteenSamplers = allocate(image, 1u, 16u); Require(BindingAllocator{}.FindBinding(sixteenSamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 32u, "sixteen logical samplers must fit with both descriptor variants"); + Require(BindingAllocator{}.FindBinding(allocate(image, 1u, 17u).layout, DescriptorBindingKind::Samplers).resources.size() == 34u, "seventeen logical samplers must fit with both descriptor variants"); + const auto twentySamplers = allocate(image, 1u, 20u); + Require(BindingAllocator{}.FindBinding(twentySamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 40u, "twenty logical samplers must fit with both descriptor variants"); + const auto thirtyTwoSamplers = allocate(image, 1u, 32u); + Require(BindingAllocator{}.FindBinding(thirtyTwoSamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 64u, "thirty-two logical samplers must fit with both descriptor variants"); const auto full = allocate(image, RuntimeAbi::SampledHeapCapacity, RuntimeAbi::SamplerHeapCapacity / 2u); Require(single.layout.ShaderDataDwords() == full.layout.ShaderDataDwords() && single.layout.memoryOffsetDword == full.layout.memoryOffsetDword && !full.layout.UsesPushData(), "runtime layout depends on resource count"); Require(full.layout.memoryOffsetDword == 0u && full.layout.DispatchThreadLimitDword() == 0u && full.layout.ShaderDataDwords() == 0u, "direct image resources allocated runtime metadata"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 27be5e3cdb..349679188c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -17,7 +17,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -29,6 +31,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -56,6 +59,30 @@ void setEnvironment(const char* name, const std::string& value) { #endif } +void removeCacheDirectory(const std::filesystem::path& directory) { + std::error_code error; +#ifdef _WIN32 + std::vector files; + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { + if (!entry.is_directory()) files.push_back(entry.path()); + } + constexpr auto dispositionInfoEx = static_cast(21); + constexpr DWORD deleteFile = 0x00000001, posixSemantics = 0x00000002; + for (const auto& file : files) { + const auto handle = CreateFileW(file.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) error.assign(static_cast(GetLastError()), std::system_category()); + require(!error, "cannot open " + file.string() + " for deletion: " + error.message()); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{handle}; + DWORD flags = deleteFile | posixSemantics; + if (!SetFileInformationByHandle(handle, dispositionInfoEx, &flags, sizeof(flags))) error.assign(static_cast(GetLastError()), std::system_category()); + require(!error, "cannot delete " + file.string() + ": " + error.message()); + } +#endif + std::filesystem::remove_all(directory, error); + require(!error, "cannot remove the shader cache test directory: " + error.message()); +} + bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; } @@ -75,7 +102,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.runtimeAbiVersion == right.runtimeAbiVersion, prefix + "runtime ABI version differs"); require(left.memoryOffsetDword == right.memoryOffsetDword, prefix + "memory offset differs"); require(left.shaderDataDwords == right.shaderDataDwords && left.imageMetadataDword == right.imageMetadataDword && left.runtimeImageCount == right.runtimeImageCount && left.runtimeImageResources == right.runtimeImageResources, prefix + "compact data layout differs"); - require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs"); + require(left.hostSubgroupSize == right.hostSubgroupSize && left.fullSubgroups == right.fullSubgroups, prefix + "host subgroup size differs"); require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ"); require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ"); require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ"); @@ -101,6 +128,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(left.specialization == right.specialization, prefix + "specialization constants differ"); require(left.workgroupMemoryDwords == right.workgroupMemoryDwords, prefix + "workgroup memory stride differs"); require(left.poisonedSrtReads == right.poisonedSrtReads, prefix + "poisoned SRT read counts differ"); + require(left.shaderDataUserWords == right.shaderDataUserWords, prefix + "shader-data user words differ"); require(left.imageTableRanges == right.imageTableRanges && left.imageTableShader == right.imageTableShader && left.imageTableFaults == right.imageTableFaults && left.imageTablePoison.size() == right.imageTablePoison.size(), prefix + "image table invocation data differs"); require(left.imageTableReadRanges == right.imageTableReadRanges, prefix + "image table read ranges differ"); for (std::size_t i = 0; i < left.imageTablePoison.size(); ++i) { @@ -169,6 +197,7 @@ RecompileResult sampleResult() { result.bdaAbiVersion = 3; result.memoryOffsetDword = 7; result.hostSubgroupSize = 32; + result.fullSubgroups = true; result.workgroupMemoryDwords = 32769u; result.vertexInputs = {{1, 4, 2}, {5, 2, 0}}; result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}}; @@ -189,6 +218,7 @@ RecompileResult sampleResult() { result.stencilReplaySafe = true; result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; + result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; result.barycentricEmulation = {true, false, true}; result.variantId = 99; result.imageTablePoison = {{{1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, 8u, 2u, 6u}, {{9u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 4u, 0xffffffffu, 19u}}; @@ -618,6 +648,7 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = 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; }); @@ -702,6 +733,64 @@ void verifyFloatModeSerialization() { require(back.request.context.floatMode == mode, "the float mode did not survive serialization"); } +int runLargeFileWrite() { + constexpr std::size_t size = (std::size_t{2} << 30u) + 4096u; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-large-file-test-" + std::to_string(std::random_device{}())); + std::error_code error; + std::filesystem::remove_all(directory, error); + std::filesystem::create_directories(directory, error); + const auto space = std::filesystem::space(directory, error); + if (error || space.available < size + (std::uintmax_t{1} << 30u)) { + std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n"; + return 77; + } + std::vector bytes; + try { + bytes.resize(size); + } catch (const std::bad_alloc&) { + std::cout << "skipping: cannot allocate a buffer of more than 2 GiB\n"; + return 77; + } + const std::array samples{0u, (std::size_t{1} << 30u) - 1u, std::size_t{1} << 30u, (std::size_t{2} << 30u) - 1u, std::size_t{2} << 30u, size - 1u}; + for (std::size_t index = 0; index < samples.size(); ++index) bytes[samples[index]] = static_cast(0xa0u + index); + const auto path = directory / "pipelines.bin"; + auto written = std::make_shared>(); + auto result = written->get_future(); + std::thread([written, &bytes, path] { + try { + written->set_value(WriteFileAtomically(path, bytes)); + } catch (...) { + written->set_exception(std::current_exception()); + } + }).detach(); + if (result.wait_for(std::chrono::seconds(200)) != std::future_status::ready) { + std::cerr << "WriteFileAtomically did not return within 200 s for a file of more than 2 GiB\n"; + std::cerr.flush(); + std::_Exit(1); + } + require(result.get(), "cannot write a cache file of more than 2 GiB"); + require(std::filesystem::file_size(path) == size, "the cache file of more than 2 GiB has the wrong size"); + { + std::ifstream file(path, std::ios::binary); + require(static_cast(file), "cannot open the cache file of more than 2 GiB"); + for (std::size_t index = 0; index < samples.size(); ++index) { + file.seekg(static_cast(samples[index])); + char value = 0; + require(file.get(value) && static_cast(value) == bytes[samples[index]], "the cache file of more than 2 GiB differs at byte " + std::to_string(samples[index])); + } + } + std::size_t entries = 0; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + static_cast(entry); + ++entries; + } + require(entries == 1u, "the cache file of more than 2 GiB left a temporary file behind"); + std::filesystem::remove_all(directory, error); + require(!error, "cannot remove the large file test directory: " + error.message()); + std::cout << "large cache file write test passed\n"; + return 0; +} + void verifyAtomicReplacement(const std::filesystem::path& directory) { struct Reader { #ifdef _WIN32 @@ -1107,7 +1196,8 @@ void verifyBindingPlanSelection() { throw std::runtime_error("binding plan accepted an unsupported buffer alignment"); }; snapshot.buffers[0].dwords[0] |= 2u; - rejectsAlignment("buffer atomic on a V# whose base is not DWORD aligned"); + const auto atomicPlan = builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot); + require(std::ranges::any_of(atomicPlan.specialization, [](const PipelineSpecializationConstant& constant) { return constant.id == PipelineSpecialization::BufferBase + 3u && constant.value == 2u; }), "a buffer atomic V# off a DWORD boundary did not record its misalignment"); snapshot.buffers[0].dwords[0] &= ~3u; info.buffers[1].typedAlignment = 2u; snapshot.buffers[1].dwords[0] |= 2u; @@ -2104,6 +2194,7 @@ int main(int argc, char** argv) { std::cout << "artifact codec tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); @@ -2136,9 +2227,7 @@ int main(int argc, char** argv) { verifyBuiltinSpecialization(); verifySpecializationLiveness(); ShaderRecompiler::ShaderDiskCache::Flush(); - std::error_code error; - std::filesystem::remove_all(directory, error); - require(!error, "cannot remove the shader cache test directory: " + error.message()); + removeCacheDirectory(directory); std::cout << "shader disk cache tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0572c8034b..3fadd9c234 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -561,9 +561,25 @@ void verifyMayEvaluate() { counter.AddArgument(&next); auto& latch = make(IrOpcode::ULessThan32, IrType::Bool, {&next, &constant(4)}); auto& sum = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&input, &constant(1)}), &constant(0)}); + auto& firstInput = make(IrOpcode::ReadFirstLane, IrType::U32, {&input, &varying}); + auto& firstLane = make(IrOpcode::ReadFirstLane, IrType::U32, {&lane, &varying}); + auto& firstActive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &varying}); + auto& firstInactive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &lane, &input}), &varying}); + plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}}; + auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)}); + auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)}); + auto& constOutside = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(4)}); + auto& constItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(2)}); + plan.srtReads[2].value = &constItself; + auto& carried = make(IrOpcode::Phi, IrType::U32, {&input}); + auto& firstCarried = make(IrOpcode::ReadFirstLane, IrType::U32, {&carried, &varying}); + carried.AddArgument(&make(IrOpcode::IAdd32, IrType::U32, {&firstCarried, &constant(1)})); + plan.srtReads.push_back({nullptr, 12u}); + auto& constFirstItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(3)}); + plan.srtReads[3].value = &make(IrOpcode::ReadFirstLane, IrType::U32, {&constFirstItself, &varying}); std::array data{4}; SrtRuntime runtime{data}; - for (const auto& [value, expected, name] : std::array, 11>{{ + for (const auto& [value, expected, name] : std::array, 21>{{ {&constant(3), true, "a constant"}, {&input, true, "user data"}, {&uniform, true, "a comparison of user data"}, @@ -575,13 +591,23 @@ void verifyMayEvaluate() { {&divergent, false, "a phi whose operands differ"}, {&latch, false, "a scalar loop counter's latch condition"}, {&sum, true, "the low word of a carry add of user data"}, + {&firstInput, true, "a first-lane read of user data"}, + {&firstLane, false, "a first-lane read of a vector register"}, + {&firstActive, true, "a first-lane read of a select whose active arm is user data"}, + {&firstInactive, false, "a first-lane read of a select whose active arm is a vector register"}, + {&constInput, true, "a constant read whose slot holds user data"}, + {&constLane, false, "a constant read whose slot holds a vector register"}, + {&constOutside, false, "a constant read past the last slot"}, + {&constItself, false, "a constant read whose slot reads itself"}, + {&firstCarried, false, "a first-lane read of a loop-carried value that reaches itself through the read"}, + {&constFirstItself, false, "a constant read whose slot reads itself through a first-lane read"}, }}) { require(Detail::Evaluator::MayEvaluate(plan, value) == expected, (std::string("MayEvaluate misjudged ") + name).c_str()); Detail::Evaluator evaluator(plan, runtime); std::uint32_t result = 0; require(evaluator.Evaluate(Detail::IrNode(value), result) == expected, (std::string("MayEvaluate disagreed with the evaluator on ") + name).c_str()); } - const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::ReadFirstLane, IrOpcode::ReadConst, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; + const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; for (std::uint16_t raw = 0; raw < static_cast(IrOpcode::Count); ++raw) { const auto opcode = static_cast(raw); if (std::ranges::find(takenAsEvaluable, opcode) != takenAsEvaluable.end()) continue; @@ -678,46 +704,48 @@ void verifySignedSrtComparison() { void verifyTypeZeroImageMaterialization() { using namespace ShaderRecompiler; - IrResourcePlan plan; - plan.srtPlanComplete = true; - plan.resourceTrackingComplete = true; - plan.userDataCount = 0u; - plan.materializationSources = {0u}; + for (const std::uint32_t type : {0u, 1u}) { + IrResourcePlan plan; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.userDataCount = 0u; + plan.materializationSources = {0u}; - const std::array words{0x12345678u, (56u << 20u) | 0x100u, 0x0000c000u, 0x00000facu, 1u, 2u, 3u, 4u}; - std::vector> values; - DescriptorSource source; - source.dwordCount = static_cast(words.size()); - for (std::uint32_t i = 0u; i < words.size(); ++i) { - auto value = std::make_unique(IrOpcode::Void, IrType::U32, i); - value->SetImmediateU32(words[i]); - source.dwords[i] = value.get(); - values.push_back(std::move(value)); - } - plan.descriptorSources.push_back(source); + const std::array words{0x12345678u, (56u << 20u) | 0x100u, 0x0000c000u, 0x00000facu | (type << 28u), 1u, 2u, 3u, 4u}; + std::vector> values; + DescriptorSource source; + source.dwordCount = static_cast(words.size()); + for (std::uint32_t i = 0u; i < words.size(); ++i) { + auto value = std::make_unique(IrOpcode::Void, IrType::U32, i); + value->SetImmediateU32(words[i]); + source.dwords[i] = value.get(); + values.push_back(std::move(value)); + } + plan.descriptorSources.push_back(source); - ImageResource image; - image.source = 0u; - image.resourceClass = ImageResourceClass::Sampled; - image.dimension = RdnaImageDimension::Dim2D; - image.read = true; - plan.info.images.push_back(image); - plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + ImageResource image; + image.source = 0u; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.read = true; + plan.info.images.push_back(image); + plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); - ResourceSnapshot snapshot; - ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); - require(snapshot.images.size() == 1u && snapshot.images[0].dwordCount == 8u && snapshot.images[0].dwords == words, - "TYPE=0 image descriptor with a nonzero base was rejected or altered during materialization"); + ResourceSnapshot snapshot; + ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); + require(snapshot.images.size() == 1u && snapshot.images[0].dwordCount == 8u && snapshot.images[0].dwords == words, + type == 0u ? "TYPE=0 image descriptor with a nonzero base was rejected or altered during materialization" : "TYPE=1 image descriptor with a nonzero base was rejected or altered during materialization"); - for (const bool atomic : {false, true}) { - image.resourceClass = ImageResourceClass::Storage; - image.read = false; - image.written = !atomic; - image.atomic = atomic; - plan.info.images[0] = image; - plan.info.runtimeImageModes[0] = ResourceMaterializer::RuntimeImageModes(image); - expectFailure([&] { ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); }, "runtime image descriptor is invalid", - atomic ? "TYPE=0 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 writable image descriptor was accepted without hardware evidence"); + for (const bool atomic : {false, true}) { + image.resourceClass = ImageResourceClass::Storage; + image.read = false; + image.written = !atomic; + image.atomic = atomic; + plan.info.images[0] = image; + plan.info.runtimeImageModes[0] = ResourceMaterializer::RuntimeImageModes(image); + expectFailure([&] { ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); }, "runtime image descriptor is invalid", + atomic ? "TYPE=0 or 1 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 or 1 writable image descriptor was accepted without hardware evidence"); + } } } @@ -869,7 +897,7 @@ void verifyGuardedDescriptorSources() { 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, 1u << 28u, 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 2u << 28u, 0u, 0u, 0u, 0u}; const std::array sampler{0u, 0u, 0u, 0u}; std::array output{}; const auto outputAddress = static_cast(reinterpret_cast(output.data())); @@ -915,6 +943,14 @@ void verifyGuardedDescriptorSources() { 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"); } + auto typeOne = valid; + typeOne[3] = 0xfacu | (1u << 28u); + fill(typeOne, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(typeOne.begin(), typeOne.end(), capture->snapshot.images[0].dwords.begin()), "guarded sources: a read-only TYPE 1 T# behind a taken branch was refused or altered"); + } fill(valid, 1u); { AgcDriver::ShaderMemory memory({}); @@ -1238,6 +1274,43 @@ void verifyImageTableBulkReads() { require(cutByRun->snapshot.tables == cutByWord->snapshot.tables && cutWordReads.wordReads == 12u && cutRunReads.wordReads == 0u && cutRunReads.bulkWords == 12u, "image table bulk reads: a record cut by an unmapped page read other words in runs than word by word"); } +void verifyImageTableBulkKeyScan() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + std::fill(guest.heap.begin(), guest.heap.end(), std::array{}); + guest.heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + guest.heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + guest.heap[3] = guest.heap[0]; + guest.materials = {{{0u, 1u, 0u, 0u}, {0u, 0x08000003u, 0u, 0u}, {0u, 1u, 0u, 0u}}}; + FillTableSrt(guest, guest.heap.data(), 32u, 4u); + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableMaterialCode, userData); + const auto capture = [&](CountingMemory& memory, bool bulk) { + SrtRuntime runtime; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + runtime.userContext = &memory; + runtime.readMemory = &CountingRead; + runtime.readSpecializationMemory = &CountingRead; + runtime.accessible = &CountingAccessible; + if (bulk) runtime.readWords = &CountingReadWords; + return CaptureResources(request, runtime); + }; + const auto begin = reinterpret_cast(guest.materials.data()); + CountingMemory byWordReads{begin, begin + sizeof(guest.materials)}; + CountingMemory byRunReads = byWordReads; + const auto byWord = capture(byWordReads, false); + const auto byRun = capture(byRunReads, true); + require(byWordReads.wordReads == guest.materials.size(), "image table bulk key scan: without a bulk reader the keys were not read one word each"); + require(byRunReads.wordReads == 0u, "image table bulk key scan: the keys were read word by word although a bulk reader exists"); + require(byRun->snapshot.tables == byWord->snapshot.tables, "image table bulk key scan: scanning the keys in bulk changed the snapshot"); + const auto& snapshot = byRun->snapshot.tables.tables.at(0); + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + require(snapshot.keys == 4u && snapshot.codes[0] == snapshot.codes[3] && snapshot.codes[0] != snapshot.codes[1] && snapshot.codes[2] == outside, "image table bulk key scan: the narrowed codes are wrong"); +} + void verifyImageTables() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -1593,11 +1666,24 @@ void verifyImageTableRegistration() { const std::vector storeCode{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0200f08u, 0x00050200u, 0xbf810000u}; const auto store = CompileTable(TableRequest(storeCode, userData)); const auto& storePlan = *store.capture->plan; - require(storePlan.info.images.size() == 1u && storePlan.info.images[0].table == 0u && storePlan.info.images[0].tableOperation == TableOperation::Unsupported, "image table registration: a table T# feeding image_store was not an unsupported table operation"); + require(storePlan.info.images.size() == 1u && storePlan.info.images[0].table == 0u && storePlan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_store was not a supported table operation"); const auto& storeMap = TableMap(*store.result); const auto storeFault = storeMap[Abi::TableHeader(0) + Abi::TableFault]; - require((storeFault & Abi::PoisonFlag) != 0u && store.result->imageTablePoison.at(storeFault & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::UnsupportedOperation) && store.result->imageTableFaults != 0u, "image table registration: the store's table does not fault on use"); - require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an unsupported table bound elements"); + const auto* storeTable = TableBinding(*store.result, DescriptorRole::ImageStorageTable); + require((storeFault & Abi::PoisonFlag) == 0u && storeTable != nullptr && storeTable->count != 0u && std::all_of(storeTable->imageWritten.begin(), storeTable->imageWritten.end(), [](bool written) { return written; }), "image table registration: the store's table was not bound as written storage elements"); + require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: a store-only table bound sampled elements"); + const std::array, 3> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}, {0xf0440108u, "image_atomic_add"}}}; + for (const auto& [instruction, name] : unsupportedStores) { + auto code = storeCode; + code[8] = instruction; + const auto unsupported = CompileTable(TableRequest(code, userData)); + const auto& plan = *unsupported.capture->plan; + require(plan.info.images.size() == 1u && plan.info.images[0].table == 0u && (plan.info.images[0].tableOperation == TableOperation::Unsupported || plan.info.images[0].mipMode == ImageMipMode::DynamicStorage), (std::string("image table registration: a table T# feeding ") + name + " was not planned as an unsupported or dynamic-mip table store").c_str()); + const auto fault = TableMap(*unsupported.result)[Abi::TableHeader(0) + Abi::TableFault]; + require((fault & Abi::PoisonFlag) != 0u && unsupported.result->imageTablePoison.at(fault & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::UnsupportedOperation) && unsupported.result->imageTableFaults != 0u, (std::string("image table registration: the ") + name + " table does not fault on use").c_str()); + const auto* storage = TableBinding(*unsupported.result, DescriptorRole::ImageStorageTable); + require(storage == nullptr || storage->count == 0u, (std::string("image table registration: the unsupported ") + name + " bound storage table elements").c_str()); + } const std::vector compareCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0fc0108u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; auto compareRequest = TableRequest(compareCode, userData); @@ -2675,6 +2761,45 @@ void verifyHalfWaveReduction() { expectFailure([&] { static_cast(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup"); } +void verifyEntryLaneProofBudget() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; + std::vector code{0xbe98047eu, 0x7e160f00u, 0x8786187eu, 0xbeea287eu, 0xd501000du, 0x001a16c1u}; + for (const auto control : {0xff01110du, 0xff01120du, 0xff01140du, 0xff01180du}) code.insert(code.end(), {0x261a1afau, control}); + code.insert(code.end(), {0xd778100cu, 0x0305830du, 0x261a190du, 0xbefe046au, 0xd7600006u, 0x00013f0du, 0xd7600007u, 0x00017f0du, 0x83880706u, 0x7e020208u, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::vector savedMaskLoops; + for (std::uint32_t loop = 0; loop < 3u; ++loop) { + std::vector savedMaskLoop{0xbea2047eu, 0xbea40380u}; + savedMaskLoop.insert(savedMaskLoop.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu}); + const auto branch = 0xbf850000u | static_cast(2 - static_cast(savedMaskLoop.size() + 3u)); + savedMaskLoop.insert(savedMaskLoop.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u}); + savedMaskLoops.insert(savedMaskLoops.end(), savedMaskLoop.begin(), savedMaskLoop.end()); + } + code.insert(code.begin(), savedMaskLoops.begin(), savedMaskLoops.end()); + + ShaderPixelStageInfo pixel{}; + pixel.inputAddr = PixelInputBit(PixelInput::PositionX); + pixel.posX = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64u; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + const auto spirv = Recompile(request).spirv; + std::uint32_t reductions = 0; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == static_cast(spv::OpGroupNonUniformUMin) && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++reductions; + } + require(reductions == 1u, "a wave64 pixel UMin scan after loop-carried saved EXEC masks did not use a 32-lane subgroup reduction"); +} + void verifyNarrowVertexWave() { using namespace ShaderRecompiler; static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; @@ -2930,6 +3055,14 @@ 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"); + require(mesh.outputPrimitiveSize == 3u && replay.request.graphics->mesh->outputPrimitiveSize == 3u, "a mesh configuration does not output triangles by default"); + otherMesh = mesh; + otherMesh.outputPrimitiveSize = 2u; + other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; + const auto lineReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(other)); + require(lineReplay.request.graphics.has_value() && lineReplay.request.graphics->mesh.has_value() && lineReplay.request.graphics->mesh->outputPrimitiveSize == 2u, "the mesh output primitive size was lost in serialization"); + RecompileCacheKey::Build(other, key); + require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh output primitive size"); RecompileRequest hull{}; hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; hull.context.waveSize = 64; @@ -3306,7 +3439,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQR4AAAA=", "new requests did not use serialization version 30"); + require(requestPrefix(encoded, 8u) == "NVNQQSAAAAA=", "new requests did not use serialization version 32"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3424,7 +3557,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR8AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSEAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3515,6 +3648,7 @@ void verifyVertexInfoVersion19() { 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 version20Vertex = flagged; + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -3523,6 +3657,7 @@ void verifyVertexInfoVersion19() { 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.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -3570,21 +3705,25 @@ void verifyVertexInfoVersion19() { const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version29 = sampledBytes; + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -4547,6 +4686,16 @@ void verifyUnusedUnnormalizedSampler() { snapshot.samplers[0].dwords[2] = 0x05500180u; require(atBaseLevel(levelZero) == std::vector{false}, "unnormalized samplers: an S# with a LOD bias was flagged as exact at the base level"); snapshot.samplers[0].dwords[2] = 0x05500000u; + auto implicitUse = levelZero; + implicitUse.samplers[0].uses = SamplerUseImplicitLod; + snapshot.samplers[0].dwords[1] = 0x00fff100u; + snapshot.samplers[0].dwords[2] = 0x01500180u; + require(atBaseLevel(implicitUse) == std::vector{true}, "unnormalized samplers: an image read through an S# without a mip filter was not flagged as exact at its base level"); + require(atBaseLevel(selectedLod) == std::vector{true}, "unnormalized samplers: an image read at a selected LOD through an S# without a mip filter was not flagged as exact at its base level"); + snapshot.samplers[0].dwords[2] = 0x05500180u; + require(atBaseLevel(implicitUse) == std::vector{false}, "unnormalized samplers: an image read through an S# with a point mip filter was flagged as exact at its base level"); + snapshot.samplers[0].dwords[1] = 0x00fff000u; + snapshot.samplers[0].dwords[2] = 0x05500000u; snapshot.samplers[0].dwords[0] = 0x00000092u; const auto normalized = populate(info); require(normalized[0].imageUnnormalized == std::vector{false} && normalized[1].samplerUnnormalized == std::vector{false}, "unnormalized samplers: a normalized S# was flagged"); @@ -5387,10 +5536,16 @@ int main(int argc, char** argv) { std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--entry-lane-proof-budget") { + verifyEntryLaneProofBudget(); + std::cout << "The wave64 pixel entry-lane proof handles loop-carried saved EXEC masks on a 32-lane host\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--bindless") { verifyImageTableKeys(); verifyImageTables(); verifyImageTableBulkReads(); + verifyImageTableBulkKeyScan(); verifyImageTableSrtKey(); verifyImageTableMaskedOffset(); verifyImageTableKeyUnion(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp new file mode 100644 index 0000000000..ae0b1fb772 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp @@ -0,0 +1,57 @@ +#include "BdaAbi.hpp" +#include "ControlFlow/RequestSerializer.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) try { + if (argc != 2) return 2; + using namespace ShaderRecompiler; + const std::filesystem::path directory = argv[1]; + std::filesystem::create_directories(directory); + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + std::array srt{}; + srt[0] = 0x12345678u; + srt[1] = 0xffffffffu; + srt[2] = 0xffffffffu; + srt[3] = 0x20000000u; + srt[16] = 0x100000u; + srt[18] = 16u; + srt[19] = 0xfacu; + const std::array userData{0x200000u, 0u}; + const std::array regions{{{0x200000u, std::as_bytes(std::span(srt))}}}; + const std::array capabilities{spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userData = userData; + request.context.memory = regions; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.layout.pushConstantSizeBytes = 128u; + for (const auto mode : {0u, 1u}) { + srt[20] = mode; + std::ofstream file(directory / (mode == 0u ? "inactive.req" : "invalid.req"), std::ios::binary); + file << RequestSerializer{}.Serialize(request); + if (!file) throw std::runtime_error("cannot write synthetic replay request"); + } + std::puts("synthetic guarded replay requests written"); + return 0; +} catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake new file mode 100644 index 0000000000..9b58c1d485 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake @@ -0,0 +1,30 @@ +foreach(variable TOOL CAPTURE) + if(NOT DEFINED ${variable}) + message(FATAL_ERROR "${variable} is not set") + endif() +endforeach() + +execute_process(COMMAND "${TOOL}" --prepare "${CAPTURE}/invalid.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "0" OR NOT output MATCHES "prepared: [0-9]+ SPIR-V words") + message(FATAL_ERROR "preparation of the synthetic request failed: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --materialize "${CAPTURE}/inactive.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "0" OR NOT output MATCHES "materialized: [0-9]+ SPIR-V words") + message(FATAL_ERROR "inactive image replay failed: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --materialize "${CAPTURE}/invalid.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "1" OR NOT output MATCHES "capture failed: runtime image descriptor is invalid") + message(FATAL_ERROR "active invalid image replay did not fail at capture: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --materialize "${CAPTURE}/invalid.req" "${CAPTURE}/inactive.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "1" OR NOT output MATCHES "capture failed: runtime image descriptor is invalid" OR NOT output MATCHES "materialized: [0-9]+ SPIR-V words") + message(FATAL_ERROR "replay did not continue after a failed request: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --maintenance8 --materialize "${CAPTURE}/inactive.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "0" OR NOT output MATCHES "materialized: [0-9]+ SPIR-V words") + message(FATAL_ERROR "prepared replay with maintenance8 failed: ${code}\n${output}${errors}") +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index 79616003ec..695b81c582 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -291,6 +291,90 @@ OpFunctionEnd } } +void testLargeFunctionArray() { + constexpr std::uint32_t elementCount = 1000; + const auto source = std::string(R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpExtension "SPV_KHR_storage_buffer_storage_class" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %input Block +OpMemberDecorate %input 0 Offset 0 +OpDecorate %inputBuffer DescriptorSet 0 +OpDecorate %inputBuffer Binding 0 +OpDecorate %secondInputBuffer DescriptorSet 0 +OpDecorate %secondInputBuffer Binding 1 +OpDecorate %output Block +OpMemberDecorate %output 0 Offset 0 +OpDecorate %outputBuffer DescriptorSet 0 +OpDecorate %outputBuffer Binding 2 +OpDecorate %flag Block +OpMemberDecorate %flag 0 Offset 0 +OpDecorate %flagBuffer DescriptorSet 0 +OpDecorate %flagBuffer Binding 3 +OpDecorate %array ArrayStride 4 +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%small = OpTypeInt 8 0 +%function = OpTypeFunction %void +%zero = OpConstant %uint 0 +%scope = OpConstant %uint 2 +%semantics = OpConstant %uint 264 +%length = OpConstant %uint )") + std::to_string(elementCount) + R"( +%array = OpTypeArray %uint %length +%arrayPointer = OpTypePointer Function %array +%arrayPointerStorage = OpTypePointer StorageBuffer %array +%input = OpTypeStruct %array +%inputPointer = OpTypePointer StorageBuffer %input +%flag = OpTypeStruct %uint +%flagPointer = OpTypePointer StorageBuffer %flag +%uintPointerStorage = OpTypePointer StorageBuffer %uint +%output = OpTypeStruct %array +%outputPointer = OpTypePointer StorageBuffer %output +%inputBuffer = OpVariable %inputPointer StorageBuffer +%secondInputBuffer = OpVariable %inputPointer StorageBuffer +%outputBuffer = OpVariable %outputPointer StorageBuffer +%flagBuffer = OpVariable %flagPointer StorageBuffer +%main = OpFunction %void None %function +%entry = OpLabel +%local = OpVariable %arrayPointer Function +%flagPointerValue = OpAccessChain %uintPointerStorage %flagBuffer %zero +%flagValue = OpLoad %uint %flagPointerValue +%condition = OpINotEqual %bool %flagValue %zero +OpSelectionMerge %merge None +OpBranchConditional %condition %then %else +%then = OpLabel +%inputArray = OpAccessChain %arrayPointerStorage %inputBuffer %zero +%inputValue = OpLoad %array %inputArray +OpStore %local %inputValue +OpBranch %merge +%else = OpLabel +%secondInputArray = OpAccessChain %arrayPointerStorage %secondInputBuffer %zero +%secondInputValue = OpLoad %array %secondInputArray +OpStore %local %secondInputValue +OpBranch %merge +%merge = OpLabel +OpMemoryBarrier %scope %semantics +%localValue = OpLoad %array %local +%outputArray = OpAccessChain %arrayPointerStorage %outputBuffer %zero +OpStore %outputArray %localValue +OpReturn +OpFunctionEnd +)"; + try { + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + spvtools::SpirvTools validator(SPV_ENV_VULKAN_1_1); + check(validator.Validate(output), "large function array optimizer output is invalid"); + check(output.size() < input.size() * 4u, "large function array was scalarized"); + } catch (const std::exception& error) { + check(false, std::string("large function array: ") + 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"); @@ -457,6 +541,7 @@ int main() { const char* mode = std::getenv("APS5_SPIRV_OPT"); const bool optimizationEnabled = mode == nullptr || std::string(mode) != "none"; testNarrowConversions(optimizationEnabled); + testLargeFunctionArray(); testSpecializedSelectionExitSpirv16(); testInvalidInputIsRejected(); testSpecializedSelectionExit(); diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp index 38ad2f43ea..f0f999a04c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -135,9 +135,16 @@ void run() { input[0].resource.fields[2] = records; Require(live.TryGet(100, input) == stored, "record count rebuilt a vertex layout"); Require(VertexBufferReadSize(input[0], 5, 1) == 176u, "record count changed the direct fetch extent"); - if (records != 0) Require(VertexBufferExtent(input[0]) == 32u * records, "cached layout froze the indirect descriptor extent"); - else fails([&] { VertexBufferExtent(input[0]); }); + if (records != 0) { + Require(VertexBufferExtent(input[0]) == 32u * (records - 1u) + 16u, "cached layout froze the indirect descriptor extent"); + Require(VertexBufferExtent(input[0]) == VertexBufferReadSize(input[0], records - 1u, 1), "the indirect extent is not the fetch of the last record"); + } else { + fails([&] { VertexBufferExtent(input[0]); }); + } } + auto last = attribute(); + last.resource.fields[0] += 16u; + Require(16u + VertexBufferExtent(last) == 32u * 128u, "the last element of each record was copied past the descriptor's records"); input[0].resource.fields[1] = 0; input[0].resource.fields[3] |= 2u << 28u; input[0].resource.fields[2] = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp new file mode 100644 index 0000000000..9278d67787 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp @@ -0,0 +1,331 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t VertexBuffers = 3; +constexpr std::size_t VertexTableOffset = 0x300; +constexpr std::size_t VertexDataOffset = 0x800; +constexpr std::size_t ColorOffset = 0; +constexpr std::size_t ColorBytes = 65536; +constexpr std::uint32_t RenderTargetTileMode = 0x1b; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, + 0xbf8cc07fu, + 0xe00c2000u, 0x80010a05u, 0xe00c2000u, 0x80020e05u, 0xe00c2000u, 0x80031205u, + 0xbf8c3f70u, + 0x0602030au, 0x0604050bu, 0x0606070cu, 0x0608090du, + 0x0602030eu, 0x0604050fu, 0x06060710u, 0x06080911u, + 0x06020312u, 0x06040513u, 0x06060714u, 0x06080915u, + 0xf80008cfu, 0x04030201u, + 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array VertexCodeUser8 = [] { + auto code = VertexCode; + for (const std::size_t load : {10u, 12u, 14u}) code[load] |= 4u; + return code; +}(); + +alignas(256) constexpr std::array PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelColorWord{0x7e100280u, 0x7e120280u, 0x7e1402ffu, 0x3f008081u, 0x7e1602ffu, 0x3e7cfcfdu, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelDescriptorStore{0x7e0202f2u, 0x7e040280u, 0xe0701000u, 0x80010102u, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelStore{0x7e1a0280u, 0x7e1802f2u, 0xdc708000u, 0x00020c0du, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header ReadHeader{}; +alignas(8) Header StoreHeader{}; +alignas(8) Header DescriptorStoreHeader{}; +alignas(8) Header ColorWordHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +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, "address passes: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + void Map(bool writable) const { GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); } + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records, std::uint32_t word3) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, word3}; +} + +void FillVertices(const GuestBlock& block) { + for (std::uint32_t buffer = 0; buffer < VertexBuffers; ++buffer) { + const auto vertices = BufferDescriptor(block.At(VertexDataOffset + buffer * 0x100u), 16, 3, 0x0104dfacu); + std::memcpy(block.Bytes(VertexTableOffset + buffer * 16u), vertices.data(), sizeof(vertices)); + } +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +struct Step { + const std::uint32_t* pixel; + std::uint64_t pointer; + std::array descriptor{}; +}; + +void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps, bool linear = false) { + const auto color = target.At(ColorOffset); + const auto vertex = reinterpret_cast(VertexCodeUser8.data()); + const auto table = vertices.At(VertexTableOffset); + const float scale = 32.0f; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, linear ? 0x9000000u : 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(scale)}, {0x110, std::bit_cast(scale)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : std::initializer_list>{{0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, {0x00b, 8u << 1u}, {0x00c, 0}, {0x00d, 0}}) Append(commands, 0x76, {offset, value}); + for (const auto& step : steps) { + const auto pixel = reinterpret_cast(step.pixel); + Append(commands, 0x76, {0x008, static_cast(pixel >> 8u), static_cast(pixel >> 40u)}); + Append(commands, 0x76, {0x00e, static_cast(step.pointer), static_cast(step.pointer >> 32u)}); + Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]}); + Append(commands, 0x2d, {3, 2}); + } + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "address passes: the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +AgcDriver::Graphics::DrawPassCounts AddressDraws() { + return AgcDriver::Graphics::DrawPassTotals()[1]; +} + +AgcDriver::Graphics::DrawPassCounts OtherDraws() { + return AgcDriver::Graphics::DrawPassTotals()[0]; +} + +std::array StoredDword(const GuestBlock& data, std::size_t offset) { + return BufferDescriptor(data.At(offset), 0, 4, 0x31016facu); +} + +void Expect(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("address passes: " + what); +} + +std::string Counts(const AgcDriver::Graphics::DrawPassCounts& before, const AgcDriver::Graphics::DrawPassCounts& after, const char* draws = "address-based draws") { + return std::to_string(after.draws - before.draws) + " " + draws + ", " + std::to_string(after.passesBegun - before.passesBegun) + " began a pass, " + std::to_string(after.passesContinued - before.passesContinued) + " continued one"; +} + +void ReadersShareAPass(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelRead.data(), data.At(0)}, {PixelRead.data(), data.At(0)}}); + const auto after = AddressDraws(); + std::printf("two draws reading through the table: %s\n", Counts(before, after).c_str()); + Expect(after.draws - before.draws == 2 && after.passesContinued - before.passesContinued == 1, "a draw reading through the table after another did not continue its pass: " + Counts(before, after)); +} + +void StoreAfterTableReadBreaks(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelRead.data(), data.At(32)}, {PixelStore.data(), data.At(32)}}); + const auto after = AddressDraws(); + std::printf("a draw storing through the table after a draw reading through it: %s\n", Counts(before, after).c_str()); + Expect(after.draws - before.draws == 2 && after.passesBegun - before.passesBegun == 2, "a draw storing through the table continued the pass of a draw that read through it: " + Counts(before, after)); +} + +void ReadSeesStore(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + std::memset(data.Bytes(48), 0, 4); + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelStore.data(), data.At(48)}, {PixelRead.data(), data.At(48)}}); + const auto after = AddressDraws(); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(target.At(ColorOffset)), ColorBytes); + float stored = 0; + std::memcpy(&stored, data.Bytes(48), sizeof(stored)); + unsigned brightest = 0; + for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast(target.Bytes(ColorOffset)[i])); + std::printf("a draw reading through the table what the previous draw stored: stored %g, brightest color byte %u, %s\n", static_cast(stored), brightest, Counts(before, after).c_str()); + Expect(stored == 1.0f, "the store through the table did not reach guest memory"); + Expect(brightest == 255u, "the read through the table did not see the previous draw's store"); + Expect(after.passesBegun - before.passesBegun == 2, "a draw reading through the table continued the pass of a draw that stored through it: " + Counts(before, after)); +} + +void ReadSeesDescriptorStore(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + std::memset(data.Bytes(64), 0, 4); + const auto before = AddressDraws(); + const auto otherBefore = OtherDraws(); + Submit(vertices, target, {{PixelDescriptorStore.data(), 0, StoredDword(data, 64)}, {PixelRead.data(), data.At(64)}}); + const auto after = AddressDraws(); + const auto otherAfter = OtherDraws(); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(target.At(ColorOffset)), ColorBytes); + float stored = 0; + std::memcpy(&stored, data.Bytes(64), sizeof(stored)); + unsigned brightest = 0; + for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast(target.Bytes(ColorOffset)[i])); + std::printf("a draw reading through the table what the previous draw stored through a buffer descriptor: stored %g, brightest color byte %u, %s; the storing draw: %s\n", static_cast(stored), brightest, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str()); + Expect(otherAfter.draws - otherBefore.draws == 1, "the draw storing through a buffer descriptor was taken for an address-based one: " + Counts(otherBefore, otherAfter, "other draws")); + Expect(stored == 1.0f, "the store through the buffer descriptor did not reach guest memory"); + Expect(after.draws - before.draws == 1 && after.passesBegun - before.passesBegun == 1, "a draw reading through the table continued the pass of a draw that stored through a buffer descriptor: " + Counts(before, after)); + Expect(brightest == 255u, "the read through the table did not see the previous draw's store through a buffer descriptor"); +} + +void DescriptorStoreAfterTableReadBreaks(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + const auto before = AddressDraws(); + const auto otherBefore = OtherDraws(); + Submit(vertices, target, {{PixelRead.data(), data.At(80)}, {PixelDescriptorStore.data(), 0, StoredDword(data, 80)}}); + const auto after = AddressDraws(); + const auto otherAfter = OtherDraws(); + std::printf("a draw storing through a buffer descriptor after a draw reading through the table: %s; the storing draw: %s\n", Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str()); + Expect(after.draws - before.draws == 1 && otherAfter.draws - otherBefore.draws == 1, "the reading and the storing draw were not one address-based and one other draw"); + Expect(otherAfter.passesBegun - otherBefore.passesBegun == 1, "a draw storing through a buffer descriptor continued the pass of a draw that read through the table: " + Counts(otherBefore, otherAfter, "other draws")); +} + +void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target, bool linear) { + const bool split = std::getenv("APS5_NO_ADDRESS_READ_PASSES") != nullptr; + Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, linear); + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset)}}, linear); + const auto after = AddressDraws(); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(target.At(ColorOffset)), ColorBytes); + const unsigned red = target.Bytes(ColorOffset)[0]; + const auto what = std::string(linear ? "linear" : "tiled") + " target" + (split ? " with APS5_NO_ADDRESS_READ_PASSES=1" : ""); + std::printf("%s: a draw reading through the table the target the previous draw rendered read %s (red byte %u), %s\n", what.c_str(), red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str()); + Expect(red == (linear ? 255u : 0u), what + ": a draw reading through the table the target the previous draw rendered read " + (red == 255u ? "that draw's color" : "the bytes before it") + "; whether or not the pass breaks between them, it reads " + (linear ? "that draw's color (stored back after the draw)" : "the bytes before it (no write-back between them)")); + if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after)); +} + +} + +int main(int argc, char** argv) { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Register(VertexHeader, VertexCodeUser8.data(), sizeof(VertexCodeUser8), 2); + Register(ReadHeader, PixelRead.data(), sizeof(PixelRead), 1); + Register(StoreHeader, PixelStore.data(), sizeof(PixelStore), 1); + Register(DescriptorStoreHeader, PixelDescriptorStore.data(), sizeof(PixelDescriptorStore), 1); + Register(ColorWordHeader, PixelColorWord.data(), sizeof(PixelColorWord), 1); + GuestBlock readOnlyVertices; + GuestBlock data; + GuestBlock target; + FillVertices(readOnlyVertices); + readOnlyVertices.Map(false); + data.Map(true); + target.Map(true); + const std::string only = argc > 1 ? argv[1] : ""; + if (only.empty() || only == "--readers") ReadersShareAPass(readOnlyVertices, data, target); + if (only.empty() || only == "--store-after-read") StoreAfterTableReadBreaks(readOnlyVertices, data, target); + if (only.empty() || only == "--read-sees-store") ReadSeesStore(readOnlyVertices, data, target); + if (only.empty() || only == "--read-sees-descriptor-store") ReadSeesDescriptorStore(readOnlyVertices, data, target); + if (only.empty() || only == "--descriptor-store-after-read") DescriptorStoreAfterTableReadBreaks(readOnlyVertices, data, target); + if (only.empty() || only == "--targets") { + TargetReadAsWhenSplit(readOnlyVertices, target, false); + TargetReadAsWhenSplit(readOnlyVertices, target, true); + } + AgcDriverShutdown_nid_postfix(); + std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 81807e14b2..4f6288141b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -447,12 +447,233 @@ void DitheredCoverage(AgcDriver::VulkanDevice& device) { AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); } +void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto code : {0x00u, 0x40u, 0x80u, 0xc0u}) { + for (const auto alphaOnMsb : {false, true}) { + for (const auto cmaskMode : {0u, 0xccu, 0xffu}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, code, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = alphaOnMsb; + if (cmaskMode != 0) { + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + const auto expanded = samples == 2 ? 0xddu : samples == 4 ? 0xeeu : 0xffu; + std::memset(cmask.data, cmaskMode == 0xccu ? cmaskMode : expanded, BlockBytes); + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + } + const bool colorOne = code == 0x80u || code == 0xc0u; + const bool alphaOne = code == 0x40u || code == 0xc0u; + Color initial{}; + for (std::uint32_t channel = 0; channel < 4; ++channel) initial[channel] = channel == (alphaOnMsb ? 3u : 0u) ? static_cast(alphaOne) : static_cast(colorOne); + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + if (cmaskMode != 0) device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + if (cmaskMode != 0) { + auto recolored = target; + recolored.clearWords = {0x3c003c00u, 0x3c003c00u}; + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {recolored}}); + } + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = target; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? initial[channel] + (written[channel] - initial[channel]) / static_cast(samples) : initial[channel]; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); + Require(actual == expected, "multisampled DCC clear did not preserve undrawn samples, channels or scissor pixels"); + } + } + } + if (code == 0 && !alphaOnMsb && cmaskMode == 0) { + const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target); + Require(dccBytes + 8u <= metadata.bytes, "the DCC fixture has no adjacent-page control"); + const std::array adjacent{std::byte{0x7b}, std::byte{0x7b}, std::byte{0x7b}, std::byte{0x7b}}; + AgcDriver::GuestMemory::Write(metadata.Address() + dccBytes, adjacent); + device.ColorMetadataPass(pass); + Require(ReadBack(device, resolved) == stored, "an adjacent driver store changed the resident DCC target"); + static_cast(metadata.data)[dccBytes + 4u] = 0x7c; + AgcDriver::GuestMemory::CollectWritesUncached(metadata.Address() + dccBytes, 8u); + device.ColorMetadataPass(pass); + Require(ReadBack(device, resolved) == stored, "an adjacent CPU store changed the resident DCC target"); + const std::array same{std::byte{0}}; + AgcDriver::GuestMemory::Write(metadata.Address(), same); + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value driver DCC store did not invalidate multisampled DCC residency"); + } + if (code == 0 && alphaOnMsb && cmaskMode == 0) { + static_cast(source.data)[0] = 0; + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value CPU color store did not invalidate multisampled DCC residency"); + } + if (code == 0x40u && alphaOnMsb && cmaskMode == 0) { + static_cast(metadata.data)[0] = 0x40u; + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value CPU DCC store did not invalidate multisampled DCC residency"); + } + if (code == 0xc0u && alphaOnMsb && cmaskMode == 0) { + AgcDriver::GuestMemory::Unwatch(metadata.Address(), 4096); + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("reuse requires watched guest memory") != std::string_view::npos; + } + Require(refused, "an unwatched metadata range remained eligible for multisampled DCC reuse"); + } + if (code == 0x80u && alphaOnMsb && cmaskMode == 0) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(metadata.data); + } + { + GuestAllocations::Mutation mutation; + mutation.Add(metadata.data, metadata.bytes, true, true, true); + } + bool readRefused = false; + try { + AgcDriver::Graphics::RequireNoMultisampleTarget(source.Address(), source.bytes, "a CPU read"); + } catch (const std::runtime_error&) { + readRefused = true; + } + Require(readRefused, "releasing DCC metadata removed the unpublished color read guard"); + bool reuseRefused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + reuseRefused = std::string_view(error.what()).find("metadata was released") != std::string_view::npos; + } + Require(reuseRefused, "recreated DCC metadata reused a resident color target"); + } + programCount = 0; + } + } + } + } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, mode == 0 ? 0xff : 0, metadata.bytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + std::string_view expected = "not a supported uniform fast clear"; + if (mode == 1) { + metadata.data[0] = 0x40u; + expected = "mixed multisampled DCC metadata"; + } + if (mode == 2 || mode == 3 || mode == 7) { + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + std::memset(cmask.data, mode == 2 ? 0 : 0xcc, cmask.bytes); + if (mode == 3) cmask.data[0] = 0xdd; + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + expected = "CMASK compression or register clear"; + } + if (mode == 4) { + AgcDriver::GuestMemory::Unwatch(metadata.Address(), 4096); + expected = "initial multisampled DCC contents require watched"; + } + if (mode == 5) { + target.dccAddress = source.Address(); + expected = "overlapping multisampled DCC"; + } + if (mode == 6) { + target.dccAddress += 256u; + expected = "metadata base alignment"; + } + if (mode == 7) { + target.dccControl = 0x80u; + expected = "color transform, lossy mode"; + } + bool refused = false; + try { + if (mode == 7) device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + else Draw(device, target, false, Float32Export, Program({0.25f, 0.25f, 0.25f, 0.25f}, Float32Export)); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(refused, "unsupported initial multisampled DCC contents were not refused explicitly"); + programCount = 0; + } + } +} + } int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--dcc-fast-clear") { + const auto samples = argc == 2 ? 0u : std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; + Require(argc == 2 || samples != 0, "unknown DCC test sample count"); + try { + DccFastClearTests(*device, samples); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } + std::cout << "Multisampled DCC fast clear tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--dithered8") { try { DitheredCoverage(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp index bbd38f1f91..c3520e8346 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp @@ -42,7 +42,10 @@ alignas(256) constexpr std::array PairCode{ 0xe0741000, 0x80020302, 0xbf810000, }; -alignas(256) constexpr std::array AtomicCode{0xe0c81000, 0x80000100, 0xbf810000}; +alignas(256) constexpr std::array AtomicCode{ + 0x34040082, 0x4a040408, 0x4a020081, 0xe0c85000, 0x80000102, 0xbf8c3f70, 0x34060082, 0xe0701000, + 0x80010103, 0xbf810000, +}; constexpr std::array FormattedLoadCode(std::uint32_t format, std::uint32_t offset) { return {0x34020082, 0xe8001000u | (format << 19u) | offset, 0x80000201, 0xbf8c0000, 0xe0701000, 0x80010201, 0xbf810000}; @@ -218,20 +221,32 @@ void RunFormattedStore(AgcDriver::VulkanDevice& device, std::uint32_t viewOffset } } -void RunAtomic(AgcDriver::VulkanDevice& device) { +void DispatchAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment, std::uint32_t addend) { + Output.fill(Fill); std::vector userData; - const auto descriptor = BufferDescriptor(reinterpret_cast(Output.data()) + 2u, 64u); - userData.insert(userData.end(), descriptor.begin(), descriptor.end()); - const std::array memory{{{reinterpret_cast(AtomicCode.data()), std::as_bytes(std::span(AtomicCode))}}}; - try { - const auto result = ShaderRecompiler::Recompile(Request(AtomicCode, userData, device.Target(), memory)); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(AtomicCode.data())); - device.WaitIdle(); - } catch (const std::exception& error) { - Require(std::string(error.what()).find("buffer atomic on a V# whose base is not DWORD aligned") != std::string::npos, std::string("buffer unaligned base atomic: unexpected error: ") + error.what()); - return; + for (const auto& descriptor : {BufferDescriptor(reinterpret_cast(Output.data()) + misalignment, (Lanes + 2u) * 4u), BufferDescriptor(Output.data() + 128, Lanes * 4u)}) { + userData.insert(userData.end(), descriptor.begin(), descriptor.end()); } - throw std::runtime_error("buffer unaligned base atomic: an atomic on a V# off a DWORD boundary ran"); + userData.push_back(addend); + const std::array memory{{{reinterpret_cast(AtomicCode.data()), std::as_bytes(std::span(AtomicCode))}}}; + const auto result = ShaderRecompiler::Recompile(Request(AtomicCode, userData, device.Target(), memory)); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(AtomicCode.data())); + device.WaitIdle(); +} + +void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) { + DispatchAtomic(device, misalignment, 4u - misalignment); + const auto run = "buffer unaligned base atomic, view +" + std::to_string(misalignment) + ", offset lane * 4 + " + std::to_string(4u - misalignment); + for (std::uint32_t word = 0; word < 128u; ++word) { + const auto expected = word >= 1u && word <= Lanes ? Fill + word : Fill; + Require(Output[word] == expected, run + ": dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected)); + } + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); +} + +void RunAtomicFault(AgcDriver::VulkanDevice& device) { + DispatchAtomic(device, 2u, 0u); + for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2"); } } @@ -246,7 +261,7 @@ int main() { RunPair(*device, 1); RunPair(*device, 2); RunPair(*device, 3); - RunAtomic(*device); + for (const auto view : {1u, 2u, 3u}) RunAtomic(*device, view); RunFormattedLoad(*device, 2); RunFormattedStore(*device, 2); for (const auto view : {1u, 3u}) { @@ -257,6 +272,7 @@ int main() { RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view); RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view); } + RunAtomicFault(*device); std::puts("buffer unaligned base tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp new file mode 100644 index 0000000000..f320df84eb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp @@ -0,0 +1,307 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color565StandardInfo = 0x8040u; +constexpr std::uint32_t Color565ReversedInfo = 0x9040u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 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, 16> Programs{}; +std::size_t programCount = 0; + +using Color = std::array; + +std::uint16_t Texel(const Color& color, bool reversed) { + const auto unorm = [](float value, std::uint32_t maximum) { return static_cast(std::lround(std::fmin(std::fmax(static_cast(value), 0.0), 1.0) * maximum)); }; + const auto low = unorm(color[reversed ? 2 : 0], 31); + const auto high = unorm(color[reversed ? 0 : 2], 31); + return static_cast(low | (unorm(color[1], 63) << 5u) | (high << 11u)); +} + +std::span Program(const Color& color) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(color[channel]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R5G6B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 5_6_5 unorm target did not decode as an R5G6B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block) { + std::vector halves(block.bytes / 2u); + for (std::size_t index = 0; index < halves.size(); ++index) halves[index] = static_cast((index * 0x9e3779b9u) >> 16u); + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint16_t texel, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? texel : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, bool reversed, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 2); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3, reversed ? Color565ReversedInfo : Color565StandardInfo); + auto before = Fill(block); + struct Case { + Color color; + std::uint16_t reversedTexel; + std::uint16_t standardTexel; + }; + const std::array cases{{ + {{1.0f, 0.0f, 0.0f, 1.0f}, 0xf800, 0x001f}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x07e0, 0x07e0}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x001f, 0xf800}, + {{1.0f, 0.0f, 1.0f, 0.5f}, 0xf81f, 0xf81f}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff, 0xffff}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x0000, 0x0000}, + {{3.0f / 31.0f, 40.0f / 63.0f, 21.0f / 31.0f, 1.0f}, (3u << 11u) | (40u << 5u) | 21u, 3u | (40u << 5u) | (21u << 11u)}, + {{21.0f / 31.0f, 3.0f / 63.0f, 10.0f / 31.0f, 0.0f}, (21u << 11u) | (3u << 5u) | 10u, 21u | (3u << 5u) | (10u << 11u)}, + {{1.0f / 31.0f, 62.0f / 63.0f, 30.0f / 31.0f, 0.75f}, (1u << 11u) | (62u << 5u) | 30u, 1u | (62u << 5u) | (30u << 11u)}, + {{30.0f / 31.0f, 1.0f / 63.0f, 1.0f / 31.0f, 0.25f}, (30u << 11u) | (1u << 5u) | 1u, 30u | (1u << 5u) | (1u << 11u)}, + {{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u << 11u) | 10u, 31u | (10u << 11u)}, + }}; + for (const auto& test : cases) { + const auto& value = test.color; + const auto texel = reversed ? test.reversedTexel : test.standardTexel; + Require(Texel(value, reversed) == texel, "the test's expected texel disagrees with the 5_6_5 field layout"); + Draw(device, color, Float32Export, Program(value)); + char what[192]; + std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 5_6_5 unorm target with the %s component order", value[0], value[1], value[2], value[3], kind, reversed ? "reversed" : "standard"); + ExpectSurface(device, block, layout, before, texel, what); + before = ReadBack(device, block); + programCount = 0; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 2).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + for (const bool reversed : {true, false}) { + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, reversed, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, reversed, "linear"); + } + std::puts("5_6_5 unorm color target 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/ComputePipelineOutsideGpuMutex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp index 95513fdc4b..dd464ab921 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp @@ -21,7 +21,7 @@ void Require(bool condition, const std::string& message) { } alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; -alignas(256) std::array NewReaderCode{0xbf800000u, 0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array NewReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xe0700004u, 0x80020000u, 0xbf810000u}; struct Slot { alignas(256) std::array value{}; @@ -104,7 +104,7 @@ int main() { Require(Slots[1].done == 0, "the dispatch finished while another thread held the GPU mutex"); } WaitDone(Slots[1], "the dispatch of the new program did not finish"); - Require(Slots[1].output[0] == 0x5678u, "the dispatch of the new program stored " + std::to_string(Slots[1].output[0])); + Require(Slots[1].output[0] == 0x5678u && Slots[1].output[1] == 0x5678u, "the dispatch of the new program stored " + std::to_string(Slots[1].output[0]) + " and " + std::to_string(Slots[1].output[1])); const auto after = VulkanDevice::ComputePipelineCounts(); Require(after.underGpuMutex == before.underGpuMutex, "a compute pipeline was created under the GPU mutex"); std::cout << "compute pipeline outside GPU mutex test passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp new file mode 100644 index 0000000000..bed49aed8f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp @@ -0,0 +1,283 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t TileRenderTarget64KB = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t KeyBytes = SurfaceBytes / 256; +constexpr std::size_t KeysOffset = SurfaceBytes; +constexpr std::size_t SourceKeysOffset = SurfaceBytes + 4096; +constexpr std::size_t BlockBytes = 2 * 65536; + +alignas(256) constexpr std::array CopyKeysCode{ + 0x34020083, 0xe0341000, 0x80000201, 0xbf8c3f70, 0xe0741000, 0x80010201, 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)); +} + +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, "DCC key pre-sync: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { + const auto address = AddressOf(texels); + const auto meta = AddressOf(keys) >> 8u; + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileRenderTarget64KB << 20u) | (Type2D << 28u), + 0u, + 0u, + (1u << 20u) | (1u << 21u) | (static_cast(meta & 0xffu) << 24u), + static_cast(meta >> 8u), + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +struct Scenario { + std::uint8_t* texels; + std::uint8_t* keys; + std::uint8_t* source; + std::uint32_t* output; + std::vector copyData; + std::vector sampleData; +}; + +Scenario Prepare(std::uint8_t* block, std::uint8_t* outputBlock) { + Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, reinterpret_cast(outputBlock), std::vector(16, 0u), std::vector(24, 0u)}; + const auto source = BufferDescriptor(scenario.source, KeyBytes); + const auto destination = BufferDescriptor(scenario.keys, KeyBytes); + std::copy(source.begin(), source.end(), scenario.copyData.begin()); + std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4); + const auto results = BufferDescriptor(scenario.output, Threads * 16u); + const auto texture = TextureDescriptor(scenario.texels, scenario.keys); + std::copy(results.begin(), results.end(), scenario.sampleData.begin()); + std::copy(texture.begin(), texture.end(), scenario.sampleData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), scenario.sampleData.begin() + 12); + return scenario; +} + +void Reset(const Scenario& scenario) { + std::memset(scenario.texels, 0x55, SurfaceBytes); + std::memset(scenario.keys, 0xff, KeyBytes); + std::memset(scenario.source, 0x00, KeyBytes); + std::fill(scenario.output, scenario.output + Threads * 4u, 0xdeadbeefu); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +void RecordKeyCopy(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const auto copy = Compile(device, CopyKeysCode, scenario.copyData); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(copy, 1, 1, 1, {}, reinterpret_cast(CopyKeysCode.data())); + Require(AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "a recorded kernel that copies data over the keys is not seen as a pending key writer"); +} + +void RequireSampledClear(const Scenario& scenario, const char* what) { + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + char message[256]; + std::snprintf(message, sizeof(message), "%s: component %u of thread %u is 0x%08x, expected 0 (keys stored 0000 by the kernel)", what, index % 4u, index / 4u, scenario.output[index]); + Require(scenario.output[index] == 0u, message); + } +} + +void RunLockedSync(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + RecordKeyCopy(device, scenario); + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data())); + device.WaitIdle(); + } + const auto after = AgcDriver::Graphics::KeyWaitCounts(); + Require(after.synced > waits.synced, "a dispatch built under the GPU mutex did not wait for the kernel that copies data over the keys"); + RequireSampledClear(scenario, "a dispatch built under the GPU mutex"); +} + +void RunPresync(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + RecordKeyCopy(device, scenario); + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + const auto presyncs = AgcDriver::VulkanDevice::PresyncCounts(); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + const auto stageA = AgcDriver::VulkanDevice::PresyncCounts(); + Require(stageA.keyChecks > presyncs.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync"); + Require(stageA.keyWaits == presyncs.keyWaits + 1 && stageA.presyncs == presyncs.presyncs + 1, "stage A did not wait for the kernel that copies data over the keys before the GPU mutex"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); + } + const auto after = AgcDriver::Graphics::KeyWaitCounts(); + Require(after.synced == waits.synced, "the dispatch waited for the key writer under the GPU mutex after the pre-sync waited for it"); + RequireSampledClear(scenario, "a dispatch whose keys were pre-synced"); +} + +bool CanFillKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const std::array pattern{0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(AddressOf(scenario.keys), KeyBytes, pattern); + device.WaitIdle(); + return filled; +} + +void RunKnownKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + const std::array pattern{0u, 0u, 0u, 0u}; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(AddressOf(scenario.keys), KeyBytes, pattern), "the fill of the keys was not recorded"); + Require(AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "a recorded fill of the keys is not seen as a pending key writer"); + Require(!AgcDriver::Graphics::DccKeyWriterNeedsSync(AddressOf(scenario.keys), KeyBytes), "the keys of a recorded constant fill need a synchronization"); + } + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + const auto before = AgcDriver::VulkanDevice::PresyncCounts(); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + const auto after = AgcDriver::VulkanDevice::PresyncCounts(); + Require(after.keyChecks > before.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync"); + Require(after.presyncs == before.presyncs, "stage A waited for a fill whose keys the locked lookup derives from the pending stores"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); + } + Require(AgcDriver::Graphics::KeyWaitCounts().synced == waits.synced, "the dispatch synchronized with the GPU although the pending fill gives the keys"); + RequireSampledClear(scenario, "a dispatch behind a pending fill of the keys"); +} + +void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto before = AgcDriver::VulkanDevice::PresyncCounts(); + Require(!AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "the idle device still reports a pending key writer"); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + const auto after = AgcDriver::VulkanDevice::PresyncCounts(); + Require(after.keyChecks > before.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync"); + Require(after.keyWaits == before.keyWaits && after.presyncs == before.presyncs, "stage A waited although no recorded work writes the keys"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->CanWaitUnlocked()) { + std::puts("skipped, the device has no timeline semaphores: the pre-sync is not tested"); + return VulkanTestSkipped; + } + GuestBlock locked; + GuestBlock lockedOutput; + GuestBlock presynced; + GuestBlock presyncedOutput; + GuestBlock known; + GuestBlock knownOutput; + const auto control = Prepare(locked.Data(), lockedOutput.Data()); + const auto changed = Prepare(presynced.Data(), presyncedOutput.Data()); + const auto fills = Prepare(known.Data(), knownOutput.Data()); + if (!CanFillKeys(*device, control)) { + std::puts("skipped, host imports are unavailable: the key writers of the test are copied writers"); + return VulkanTestSkipped; + } + RunLockedSync(*device, control); + RunPresync(*device, changed); + RunNoWriter(*device, changed); + RunKnownKeys(*device, fills); + std::puts("DCC key pre-sync 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/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 3e21bfbceb..f87d9ce6b3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -48,6 +48,8 @@ struct DrawOptions { VkCompareOp depthCompare = VK_COMPARE_OP_LESS; bool stencilTest = false; VkStencilOpState stencil{}; + float minDepth = 0.0f; + float maxDepth = 1.0f; }; inline std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -126,7 +128,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil state.stencilBack = options.stencil; state.renderExtent = {Width, Height}; state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), options.minDepth, options.maxDepth}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; state.cullMode = VK_CULL_MODE_NONE; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp new file mode 100644 index 0000000000..d833893a47 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp @@ -0,0 +1,100 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::DrawOptions; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t DepthPlaneBytes = 128u * 128u * 4u; +constexpr std::size_t StencilPlaneBytes = 256u * 256u; + +alignas(4096) std::array Depth{}; +alignas(4096) std::array Stencil{}; +alignas(4096) std::array HtileDepth{}; +alignas(256) std::array Htile{}; + +VkStencilOpState StencilState(VkCompareOp compare, VkStencilOp pass, std::uint32_t reference) { + VkStencilOpState state{}; + state.failOp = VK_STENCIL_OP_KEEP; + state.passOp = pass; + state.depthFailOp = VK_STENCIL_OP_KEEP; + state.compareOp = compare; + state.compareMask = 0xffu; + state.writeMask = 0xffu; + state.reference = reference; + return state; +} + +DrawOptions StencilEquals(std::uint32_t reference) { + return {.depthTest = false, .stencilTest = true, .stencil = StencilState(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, reference)}; +} + +DrawOptions StencilWrite(std::uint32_t reference) { + return {.depthTest = false, .stencilTest = true, .stencil = StencilState(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, reference)}; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u) == DepthPlaneBytes, "the depth plane size of the test surface changed"); + Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 1u) == StencilPlaneBytes, "the stencil plane size of the test surface changed"); + const auto shaders = Compile(*device); + const Surface surface{reinterpret_cast(Depth.data()), reinterpret_cast(Stencil.data()), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const auto depth = surface.depth; + const auto stencil = surface.stencil; + + Require(Draw(*device, shaders, surface, StencilWrite(2u)) == Covered, "the stencil mark draw did not cover the target"); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "the stencil mark was not kept"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes - 4u, 0x01010101u); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "a fill shorter than the stencil plane changed the stencil"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x01020304u); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "a fill whose bytes differ changed the stencil"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x01010101u); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == 0u, "a fill of the whole stencil plane with 1 left the stencil mark 2"); + Require(Draw(*device, shaders, surface, StencilEquals(1u)) == Covered, "a fill of the whole stencil plane with 1 did not clear the stencil to 1"); + Require(Draw(*device, shaders, surface, StencilWrite(2u)) == Covered, "the stencil could not be written after the fill"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil - 4096u, StencilPlaneBytes + 8192u, 0u); + Require(Draw(*device, shaders, surface, StencilEquals(0u)) == Covered, "a fill around the whole stencil plane with 0 did not clear the stencil to 0"); + + Require(Draw(*device, shaders, surface, {.depthWrite = true}) == Covered, "the depth was not written at 0.5"); + Require(Draw(*device, shaders, surface, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0x3f800000u); + Require(Draw(*device, shaders, surface, {}) == Covered, "a fill of the whole depth plane with 1.0 did not clear the depth to 1.0"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill of the whole depth plane with 0.0 did not clear the depth to 0.0"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0xff000000u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill of the depth plane with a value outside 0 to 1 changed the depth"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes - 4u, 0x3f800000u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill shorter than the depth plane changed the depth"); + + Htile.fill(0xffffffffu); + const Surface htiled{reinterpret_cast(HtileDepth.data()), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(*device, shaders, htiled, {.depthWrite = true}) == Covered, "the depth of the surface with HTILE was not written at 0.5"); + AgcDriver::Graphics::NoteDepthMetadataFill(htiled.depth, DepthPlaneBytes, 0u); + AgcDriver::Graphics::NoteDepthMetadataFill(htiled.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, htiled, {.depthWrite = true}) == Covered, "an HTILE fill after a depth plane fill of 0.0 did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + AgcDriver::Graphics::NoteDepthMetadataFill(htiled.depth, DepthPlaneBytes, 0u); + AgcDriver::Graphics::NoteHtileDepthClear(htiled.htile, HtileBytes); + Require(Draw(*device, shaders, htiled, {}) == Covered, "an HTILE clear kernel after a depth plane fill of 0.0 did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + std::puts("depth plane fill 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 index f83e70edb5..237d3f7646 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -199,7 +199,13 @@ void Run(Context context, std::uint8_t* block) { 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"); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 1) == nullptr, "a storage access to a later level over the depth memory was taken for the depth surface"); + auto later = PlaneResource(address, Format32Float); + later.mipCount = 2; + later.baseLevel = 1; + later.lastLevel = 1; + Require(DepthSurfaceStorage(context, {}, later, 0) == nullptr, "a storage view of a later level over the depth memory was taken for the depth surface"); + Require(DepthSurfaceTexture(context, {}, later, {}) == nullptr, "a sampled view of a later level over the depth memory was taken for the depth surface"); const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); Require(depth != nullptr, "the depth plane has no storage image"); submit(recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp new file mode 100644 index 0000000000..770cd6ad19 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp @@ -0,0 +1,285 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 64; +constexpr std::uint32_t Bytes = Words * 4u; +constexpr std::uint32_t Constant = 0x00010000u; +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::size_t Piece = 65536; +constexpr std::size_t BlockBytes = 5 * Piece; + +alignas(256) std::array Table{}; + +constexpr std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t destination, std::uint32_t source0, std::uint32_t source1) { + return (opcode << 25u) | (destination << 17u) | (source1 << 9u) | source0; +} + +constexpr std::uint32_t BufferOperands = (0x80u << 24u) | (1u << 16u) | (2u << 8u) | 1u; + +alignas(256) constexpr std::array Code{ + Vop2(0x1au, 1u, 0x82u, 0u), + 0xf4080100u, 0xfa000000u, + 0xbf8cc07fu, + 0xe0301000u, BufferOperands, + 0xbf8c3f70u, + 0xf4080100u, 0xfa000010u, + 0xbf8cc07fu, + Vop2(0x25u, 2u, 0xffu, 2u), Constant, + 0xe0701000u, BufferOperands, + 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, Piece)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = Piece; + 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, "dispatch move: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~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::uint32_t* InputA() const { return reinterpret_cast(block); } + std::uint32_t* InputB() const { return reinterpret_cast(block + Piece + 12); } + std::uint32_t* InputC() const { return reinterpret_cast(block + 2 * Piece + 32); } + std::uint32_t* OutputO() const { return reinterpret_cast(block + 3 * Piece); } + std::uint32_t* OutputP() const { return reinterpret_cast(block + 4 * Piece); } + +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::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Fill(std::uint32_t* where, std::uint32_t first) { + for (std::uint32_t index = 0; index < Words; ++index) where[index] = first + index; +} + +void Reset(std::uint32_t* output) { + std::fill(output, output + Words, Sentinel); +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const std::uint32_t* input, std::uint32_t inputBytes, const std::uint32_t* output) { + const auto in = BufferDescriptor(input, inputBytes); + const auto out = BufferDescriptor(output, Bytes); + std::copy(in.begin(), in.end(), Table.begin()); + std::copy(out.begin(), out.end(), Table.begin() + 4); + const auto table = reinterpret_cast(Table.data()); + const std::vector userData{static_cast(table), static_cast(table >> 32u)}; + const std::span code(Code); + const std::span tableWords(Table); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {table, std::as_bytes(tableWords)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& compiled, const std::uint32_t* input, std::uint32_t inputBytes, const std::uint32_t* output, bool wait) { + auto result = compiled; + std::size_t elements = 0; + for (auto& binding : result.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4u && binding.bufferWritten.size() == binding.count, "dispatch move: the program's buffer bindings have another shape"); + for (std::uint32_t element = 0; element < binding.count; ++element, ++elements) { + const auto words = binding.bufferWritten[element] ? BufferDescriptor(output, Bytes) : BufferDescriptor(input, inputBytes); + std::copy(words.begin(), words.end(), binding.guestDescriptor.begin() + static_cast(element) * 4); + } + } + Require(elements == 2, "dispatch move: the program does not bind an input and an output"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + if (wait) device.WaitIdle(); +} + +void Expect(const std::uint32_t* input, const std::uint32_t* output, std::uint32_t added, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = input[tid] + added; + Require(output[tid] == expected, "dispatch move: " + what + ": lane " + std::to_string(tid) + " is " + Hex(output[tid]) + ", expected " + Hex(expected)); + } +} + +struct Step { + std::uint64_t checked, exact, moved, refused, rebuilt, kept, served; +}; + +Step Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) { + const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); + return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused, now.rebuilt - before.rebuilt, now.kept - before.kept, now.served - before.served}; +} + +void Print(const char* what, const Step& step) { + std::printf("dispatch move: %s: checked %llu, exact %llu, moved %llu, refused %llu\n", what, static_cast(step.checked), static_cast(step.exact), static_cast(step.moved), static_cast(step.refused)); +} + +bool RunTests(AgcDriver::VulkanDevice& device, const GuestBlock& guest) { + Fill(guest.InputA(), 0xa000u); + Fill(guest.InputB(), 0xb000u); + Fill(guest.InputC(), 0xc000u); + + const auto compiled = Compile(device, guest.InputA(), Bytes, guest.OutputO()); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the first dispatch"); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + + Reset(guest.OutputO()); + Run(device, compiled, guest.InputB(), Bytes, guest.OutputO(), true); + Expect(guest.InputB(), guest.OutputO(), Constant, "an input moved to another block"); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the input back where the template was built"); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputC(), Bytes, guest.OutputO(), true); + Expect(guest.InputC(), guest.OutputO(), Constant, "an input moved to a third block"); + const auto repeats = Since(start); + Print("an exact repeat between two moved ones", repeats); + const bool templates = repeats.checked != 0; + if (templates) { + Require(repeats.exact == 1 && repeats.moved == 2 && repeats.refused == 0, "dispatch move: the repeats were not served as one exact hit and two moved ones"); + } else { + std::puts("dispatch move: the device kept no template (no host imports); the moved-buffer path is not exercised"); + } + + Reset(guest.OutputO()); + Run(device, compiled, guest.InputB(), 192, guest.OutputO(), true); + Expect(guest.InputB(), guest.OutputO(), Constant, "an input moved and shortened"); + + std::array changed{}; + for (std::uint32_t index = 0; index < Words; ++index) changed[index] = 0xd000u + index * 3u; + AgcDriver::GuestMemory::Write(reinterpret_cast(guest.InputB()), std::as_bytes(std::span(changed))); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputB(), Bytes, guest.OutputO(), true); + Expect(changed.data(), guest.OutputO(), Constant, "an input moved to a range the driver stored over"); + + const auto refusals = AgcDriver::VulkanDevice::DispatchMoveCounts(); + for (std::uint32_t index = 0; index < Words; ++index) guest.OutputO()[index] = 0x7000u + index; + Run(device, compiled, guest.OutputO() + Threads, Bytes / 2, guest.OutputO(), true); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = 0x7000u + Threads + tid + Constant; + Require(guest.OutputO()[tid] == expected, "dispatch move: an input moved into the unwritten half of the dispatch's own output: lane " + std::to_string(tid) + " is " + Hex(guest.OutputO()[tid]) + ", expected " + Hex(expected)); + } + + Reset(guest.OutputO()); + Reset(guest.OutputP()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputP(), false); + Run(device, compiled, guest.OutputP(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input moved onto a range a recorded dispatch still writes"); + const auto refused = Since(refusals); + Print("two inputs moved onto written ranges", refused); + if (templates) { + Require(refused.refused == 2, "dispatch move: the inputs moved onto written ranges were not both refused"); + Require(refused.rebuilt == 2 && refused.kept == 2 && refused.served == 0, "dispatch move: the two refusals were not two rebuilds inserted beside the template"); + } + + const auto afterRefusals = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the input moved back after the refusals"); + const auto back = Since(afterRefusals); + Print("the input back after the refusals", back); + if (templates) Require(back.checked == 1 && back.exact == 1 && back.moved == 0 && back.refused == 0 && back.rebuilt == 0, "dispatch move: the input back at the template's address was not an exact hit after the refusals"); + + for (int round = 0; round < 2; ++round) { + const auto alternating = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Reset(guest.OutputO()); + Reset(guest.OutputP()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputP(), false); + Run(device, compiled, guest.OutputP(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input refused again while A alternates"); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the input at A after the refused one"); + const auto step = Since(alternating); + Print("A alternating with a refused input", step); + if (templates) Require(step.refused == 1 && step.served == 1 && step.rebuilt == 0 && step.exact == 2 && step.moved == 0, "dispatch move: a refused input rebuilt, or evicted the template of A, while A alternated with it"); + } + const auto total = AgcDriver::VulkanDevice::DispatchMoveCounts(); + std::printf("dispatch move: in total: checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", static_cast(total.checked), static_cast(total.exact), static_cast(total.moved), static_cast(total.refused), static_cast(total.filtered)); + return templates; +} + +} + +int main() { + try { + GuestBlock guest; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!RunTests(*device, guest)) return VulkanTestSkipped; + std::puts("dispatch move 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/DispatchMoveDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMoveDepth.cpp new file mode 100644 index 0000000000..1d44a9207d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMoveDepth.cpp @@ -0,0 +1,460 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Bytes = Threads * 4u; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::size_t Piece = 65536; + +alignas(256) constexpr std::array RowCode{ + 0x7e3c0300u, + 0x7e3e0280u, + 0x34060082u, + 0xe0301000u, 0x80040b03u, + 0xbf8c3f70u, + 0x4a181681u, + 0xe0701000u, 0x80000c03u, + 0xf0201108u, 0x00010b1eu, + 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ProbeCode{ + 0x7e3c0300u, + 0x7e3e020cu, + 0xf0001108u, 0x00010a1eu, + 0x34060082u, + 0xbf8c3f70u, + 0xe0701000u, 0x80000a03u, + 0xbf810000u, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock(std::size_t pieces, bool inWatch) : bytes(pieces * Piece), watched(inWatch) { + if (inWatch) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Piece)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + constexpr std::uintptr_t alignment = Piece; + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + } +#endif + } else { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(Piece, bytes)); +#endif + if (block != nullptr) std::memset(block, 0, bytes); + } + Require(block != nullptr, "dispatch move depth: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); + if (watched) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#else + munmap(block, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); +#endif + } else { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* PieceAt(std::size_t index) const { return block + index * Piece; } + +private: + std::size_t bytes = 0; + bool watched = false; + std::uint8_t* block = nullptr; +}; + +struct Buffers { + explicit Buffers(const GuestBlock& block) : block(block) {} + std::uint32_t* InputA() const { return reinterpret_cast(block.PieceAt(4)); } + std::uint32_t* InputB() const { return reinterpret_cast(block.PieceAt(5) + 12); } + std::uint32_t* InputC() const { return reinterpret_cast(block.PieceAt(6) + 32); } + std::uint32_t* Output() const { return reinterpret_cast(block.PieceAt(8)); } + +private: + const GuestBlock& block; +}; + +struct Env { + AgcDriver::VulkanDevice& device; + const Shaders& shaders; + const Buffers& buffers; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +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 RowProgram(AgcDriver::VulkanDevice& device, const void* plane, const std::uint32_t* input, const std::uint32_t* output) { + std::vector userData(20, 0u); + const auto out = BufferDescriptor(output, Bytes); + const auto texture = TextureDescriptor(plane); + const auto in = BufferDescriptor(input, Bytes); + std::copy(out.begin(), out.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(in.begin(), in.end(), userData.begin() + 16); + return Compile(device, RowCode, userData); +} + +void RunRow(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& compiled, const std::uint32_t* input, const std::uint32_t* output) { + auto result = compiled; + std::size_t elements = 0; + for (auto& binding : result.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4u, "dispatch move depth: the program's buffer bindings have another shape"); + for (std::uint32_t element = 0; element < binding.count; ++element, ++elements) { + auto* current = binding.guestDescriptor.data() + static_cast(element) * 4u; + const auto built = static_cast(current[0]) | (static_cast(current[1] & 0xffffu) << 32u); + const auto words = built == static_cast(reinterpret_cast(output)) ? BufferDescriptor(output, Bytes) : BufferDescriptor(input, Bytes); + std::copy(words.begin(), words.end(), current); + } + } + Require(elements == 2, "dispatch move depth: the program does not bind an input and an output"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(RowCode.data())); + device.WaitIdle(); +} + +std::array ReadRow(const Env& env, const void* plane, std::uint32_t y) { + std::vector userData(16, 0u); + auto* output = env.buffers.Output(); + const auto out = BufferDescriptor(output, Bytes); + const auto texture = TextureDescriptor(plane); + std::copy(out.begin(), out.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = y; + const auto program = Compile(env.device, ProbeCode, userData); + std::fill(output, output + Threads, Sentinel); + env.device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(ProbeCode.data())); + env.device.WaitIdle(); + std::array lanes{}; + std::copy(output, output + Threads, lanes.begin()); + return lanes; +} + +void FillInput(std::uint32_t* where, float first) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) where[lane] = std::bit_cast(first + static_cast(lane) / 1024.0f); +} + +void FillPlane(std::uint8_t* plane, float value) { + auto* words = reinterpret_cast(plane); + std::fill(words, words + Piece / sizeof(std::uint32_t), std::bit_cast(value)); +} + +void DrawDepth(const Env& env, const void* plane, float depth) { + const Surface surface{reinterpret_cast(plane), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const float low = depth <= 0.5f ? 0.0f : 2.0f * depth - 1.0f; + const float high = depth <= 0.5f ? 2.0f * depth : 1.0f; + Require(Draw(env.device, env.shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = low, .maxDepth = high}) == Covered, "dispatch move depth: the depth producer did not cover the surface"); +} + +struct PlaneModel { + std::array row0{}; + std::uint32_t rest = 0; + + void Set(float value) { + rest = std::bit_cast(value); + row0.fill(rest); + } + + std::array Dispatch(const std::uint32_t* input) { + std::array sums{}; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + sums[lane] = input[lane] + 1u; + row0[lane] = input[lane]; + } + return sums; + } +}; + +void Totals(const std::string& what); + +void Step(const Env& env, const ShaderRecompiler::RecompileResult& program, PlaneModel& model, const std::uint32_t* input, const std::string& what) { + const auto expected = model.Dispatch(input); + auto* output = env.buffers.Output(); + std::fill(output, output + Threads, Sentinel); + RunRow(env.device, program, input, output); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(output[lane] == expected[lane], "dispatch move depth: " + what + ": lane " + std::to_string(lane) + " is " + Hex(output[lane]) + ", expected " + Hex(expected[lane])); + } + Totals(what); +} + +void Verify(const Env& env, const void* plane, const PlaneModel& model, const std::string& what) { + for (const std::uint32_t y : {0u, 1u, Height - 1u}) { + const auto lanes = ReadRow(env, plane, y); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto expected = y == 0 ? model.row0[lane] : model.rest; + Require(lanes[lane] == expected, "dispatch move depth: " + what + ": row " + std::to_string(y) + ", texel " + std::to_string(lane) + " of the plane is " + Hex(lanes[lane]) + ", expected " + Hex(expected)); + } + } +} + +struct MoveSteps { + std::uint64_t checked, exact, moved, refused; +}; + +MoveSteps Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) { + const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); + return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused}; +} + +void Totals(const std::string& what) { + const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); + std::printf("dispatch move depth: %s: in total checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", what.c_str(), static_cast(now.checked), static_cast(now.exact), static_cast(now.moved), static_cast(now.refused), static_cast(now.filtered)); +} + +void DescribeBindings(const ShaderRecompiler::RecompileResult& program) { + for (const auto& binding : program.bindings) { + std::string written; + for (const bool bit : binding.bufferWritten) written += bit ? '1' : '0'; + std::printf("dispatch move depth: binding role %d kind %d count %u, %zu descriptor words, buffers written [%s], images written %zu\n", static_cast(binding.role), static_cast(binding.kind), binding.count, binding.guestDescriptor.size(), written.c_str(), binding.imageWritten.size()); + } +} + +void Print(const char* what, const MoveSteps& step) { + std::printf("dispatch move depth: %s: checked %llu, exact %llu, moved %llu, refused %llu\n", what, static_cast(step.checked), static_cast(step.exact), static_cast(step.moved), static_cast(step.refused)); +} + +void Warm(const Env& env, std::uint8_t* plane) { + FillPlane(plane, 0.5f); + PlaneModel model; + model.Set(0.5f); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + Step(env, program, model, env.buffers.InputA(), "warm-up: the first input"); + Step(env, program, model, env.buffers.InputB(), "warm-up: the second input"); + Step(env, program, model, env.buffers.InputC(), "warm-up: the third input"); +} + +bool Control(const Env& env, std::uint8_t* plane) { + FillPlane(plane, 0.5f); + PlaneModel model; + model.Set(0.5f); + const auto refusals = AgcDriver::Graphics::ShaderResources::DepthGuardRefusals(); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + DescribeBindings(program); + Step(env, program, model, env.buffers.InputA(), "control: the first dispatch"); + Totals("control, after the first dispatch"); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Step(env, program, model, env.buffers.InputB(), "control: an input moved"); + Totals("control, after an input moved"); + Step(env, program, model, env.buffers.InputC(), "control: the input moved again"); + Totals("control, after the input moved again"); + Step(env, program, model, env.buffers.InputC(), "control: an exact repeat"); + Totals("control, after an exact repeat"); + const auto steps = Since(start); + Print("control, a plane without a depth surface", steps); + Verify(env, plane, model, "control"); + Require(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() == refusals, "dispatch move depth: the depth guard refused a proof with no depth surface in sight"); + Require(steps.refused == 0, "dispatch move depth: the control's repeats were refused"); + if (steps.checked == 0) return false; + Require(steps.checked == 3 && steps.moved == 3, "dispatch move depth: the control's repeats were not all served by the template of the first dispatch"); + return true; +} + +void Mirror(const Env& env, std::uint8_t* plane, const char* where) { + const std::string at = std::string(where) + ", the mirror: "; + FillPlane(plane, 0.5f); + PlaneModel model; + DrawDepth(env, plane, 0.25f); + model.Set(0.25f); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + Step(env, program, model, env.buffers.InputA(), at + "the first dispatch"); + Verify(env, plane, model, at + "after the first dispatch"); + DrawDepth(env, plane, 0.75f); + model.Set(0.75f); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Step(env, program, model, env.buffers.InputB(), at + "an input moved after the depth was drawn again"); + Verify(env, plane, model, at + "after the moved dispatch"); + Step(env, program, model, env.buffers.InputB(), at + "an exact repeat"); + DrawDepth(env, plane, 0.25f); + model.Set(0.25f); + Step(env, program, model, env.buffers.InputC(), at + "the input moved again after another redraw"); + Verify(env, plane, model, at + "after the last dispatch"); + Print((at + "repeats and redraws").c_str(), Since(start)); +} + +void Appears(const Env& env, std::uint8_t* plane, const char* where, bool moved, bool templates) { + const std::string at = std::string(where) + (moved ? ", a surface appears, input moved: " : ", a surface appears, exact repeat: "); + FillPlane(plane, 0.125f); + PlaneModel model; + model.Set(0.125f); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + Step(env, program, model, env.buffers.InputA(), at + "the first dispatch, no depth surface yet"); + DrawDepth(env, plane, 0.25f); + model.Set(0.25f); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + const auto refusals = AgcDriver::Graphics::ShaderResources::DepthGuardRefusals(); + Step(env, program, model, moved ? env.buffers.InputB() : env.buffers.InputA(), at + "the dispatch after the surface appeared"); + std::printf("dispatch move depth: %sthe depth guard refused %llu proofs\n", at.c_str(), static_cast(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() - refusals)); + if (templates) Require(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() > refusals, "dispatch move depth: " + at + "the template over the plain storage image was proved current although a depth surface covers its memory"); + Print((at + "the dispatch after the surface appeared").c_str(), Since(start)); + Totals(at + "after that dispatch"); + Verify(env, plane, model, at + "after that dispatch"); + DrawDepth(env, plane, 0.75f); + model.Set(0.75f); + Step(env, program, model, env.buffers.InputC(), at + "the input moved after the next redraw"); + Verify(env, plane, model, at + "after the last dispatch"); +} + +bool RunTests(const Env& env, const GuestBlock& watched, const GuestBlock& unwatched, std::size_t pass) { + const auto inWatch = pass * 10; + const auto outside = pass * 3; + FillInput(env.buffers.InputA(), 0.0625f); + FillInput(env.buffers.InputB(), 0.109375f); + FillInput(env.buffers.InputC(), 0.15625f); + Warm(env, watched.PieceAt(9)); + const bool templates = Control(env, watched.PieceAt(inWatch)); + if (!templates) std::puts("dispatch move depth: the device kept no template (no host imports); the moved-buffer path is not exercised"); + Mirror(env, watched.PieceAt(inWatch + 1), "inside the write watch"); + Appears(env, watched.PieceAt(inWatch + 2), "inside the write watch", true, templates); + Appears(env, watched.PieceAt(inWatch + 3), "inside the write watch", false, templates); + Mirror(env, unwatched.PieceAt(outside), "outside the write watch"); + Appears(env, unwatched.PieceAt(outside + 1), "outside the write watch", true, templates); + Appears(env, unwatched.PieceAt(outside + 2), "outside the write watch", false, templates); + return templates; +} + +} + +int main() { + try { + const GuestBlock watched(20, true); + const GuestBlock unwatched(6, false); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + const Buffers buffers(watched); + const Env env{*device, shaders, buffers}; + bool templates = true; +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; + std::size_t pass = 0; + for (const auto mode : {AgcDriver::Graphics::ImportWatch::Watch, AgcDriver::Graphics::ImportWatch::Unwatch}) { + AgcDriver::Graphics::SetImportWatch(context, mode); + std::printf("dispatch move depth: pass %zu, imports %s\n", pass, mode == AgcDriver::Graphics::ImportWatch::Watch ? "under the write watch" : "left unwatched"); + templates = RunTests(env, watched, unwatched, pass++) && templates; + } +#else + templates = RunTests(env, watched, unwatched, 0); +#endif + if (!templates) return VulkanTestSkipped; + std::puts("dispatch move depth 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/DivFixupF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF16DenormalModes.cpp new file mode 100644 index 0000000000..12b3fe84c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF16DenormalModes.cpp @@ -0,0 +1,138 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 10; +constexpr std::uint32_t Threads = 1024; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = {0x0000u, 0x8000u, 0x0001u, 0x83ffu, 0x0200u, 0x0400u, 0x3c00u, 0xfc00u, 0x7e00u, 0x7d00u}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040082u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd75f000au, 0x041a0b04u, + 0xe0701000u, 0x80010a02u, 0xbf810000u, +}; + +struct Mode { + const char* name; + std::optional value; + bool flushed; + bool ieee; +}; + +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 Fill() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t row = tid < ValueCount * ValueCount * ValueCount ? tid : 0u; + Input[tid * Inputs] = Values[row / (ValueCount * ValueCount)]; + Input[tid * Inputs + 1u] = Values[row / ValueCount % ValueCount]; + Input[tid * Inputs + 2u] = Values[row % ValueCount]; + Input[tid * Inputs + 3u] = 0u; + } +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Fixup(std::uint32_t quotient, std::uint32_t denominator, std::uint32_t numerator, bool flushed, bool ieee) { + const auto magnitude = [](std::uint32_t half) { return half & 0x7fffu; }; + const auto flush = [&](std::uint32_t half) { return flushed && magnitude(half) < 0x0400u ? half & 0x8000u : half; }; + denominator = flush(denominator); + numerator = flush(numerator); + const auto nan = [&](std::uint32_t half) { return magnitude(half) > 0x7c00u; }; + const auto infinite = [&](std::uint32_t half) { return magnitude(half) == 0x7c00u; }; + const auto zero = [&](std::uint32_t half) { return magnitude(half) == 0u; }; + const std::uint32_t sign = (denominator ^ numerator) & 0x8000u; + if (nan(numerator)) return ieee ? numerator | 0x0200u : numerator; + if (nan(denominator)) return ieee ? denominator | 0x0200u : denominator; + if ((zero(denominator) && zero(numerator)) || (infinite(denominator) && infinite(numerator))) return 0xfe00u; + if (zero(denominator) || infinite(numerator)) return sign | 0x7c00u; + if (infinite(denominator) || zero(numerator)) return sign; + if (nan(quotient)) return sign | 0x7c00u; + return sign | magnitude(quotient); +} + +void Check(const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t expected = Fixup(in[0], in[1], in[2], mode.flushed, mode.ieee); + Require(Output[tid] == expected, std::string("v_div_fixup_f16 ") + mode.name + " of " + Hex(in[0]) + ", " + Hex(in[1]) + ", " + Hex(in[2]) + " is " + Hex(Output[tid]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Fill(); + const Mode modes[] = { + {"without a known float mode", std::nullopt, false, false}, + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true, true}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true, false}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false, true}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false, false}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true, true}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true, false}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false, true}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false, false}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_div_fixup_f16 denormal mode 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/DivFixupF64DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp new file mode 100644 index 0000000000..b609850c3f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 10; +constexpr std::uint32_t Threads = 1024; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020085u, 0x34040083u, 0xe0381000u, 0x80000401u, 0xe0341010u, 0x80000801u, 0xbf8c3f70u, 0xd560000au, + 0x04220d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint64_t Values[ValueCount] = { + 0x0000000000000000ull, 0x8000000000000000ull, 0x0000000000000001ull, 0x800fffffffffffffull, 0x0010000000000000ull, + 0x3ff0000000000000ull, 0xbff0000000000000ull, 0x7fefffffffffffffull, 0x7ff0000000000000ull, 0x7ff8000000000000ull, +}; + +struct Mode { + const char* name; + std::optional value; + bool flushed; + bool ieee; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +std::array Sources(std::uint32_t tid) { + const std::uint32_t row = tid < ValueCount * ValueCount * ValueCount ? tid : 0u; + return {Values[row / (ValueCount * ValueCount)], Values[row / ValueCount % ValueCount], Values[row % ValueCount]}; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sources = Sources(tid); + for (std::uint32_t index = 0; index < sources.size(); ++index) { + Input[tid * Inputs + index * 2u] = static_cast(sources[index]); + Input[tid * Inputs + index * 2u + 1u] = static_cast(sources[index] >> 32u); + } + Input[tid * Inputs + 6u] = 0u; + Input[tid * Inputs + 7u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint64_t Fixup(std::uint64_t quotient, std::uint64_t denominator, std::uint64_t numerator, bool flushed, bool ieee) { + constexpr std::uint64_t Magnitude = 0x7fffffffffffffffull; + constexpr std::uint64_t Infinity = 0x7ff0000000000000ull; + const auto flush = [&](std::uint64_t value) { return flushed && (value & Infinity) == 0u ? value & ~Magnitude : value; }; + denominator = flush(denominator); + numerator = flush(numerator); + const auto nan = [&](std::uint64_t value) { return (value & Magnitude) > Infinity; }; + const auto infinite = [&](std::uint64_t value) { return (value & Magnitude) == Infinity; }; + const auto zero = [&](std::uint64_t value) { return (value & Magnitude) == 0u; }; + const auto exponent = [](std::uint64_t value) { return static_cast((value >> 52u) & 0x7ffu); }; + const std::uint64_t sign = (denominator ^ numerator) & ~Magnitude; + if (nan(numerator)) return ieee ? numerator | 0x0008000000000000ull : numerator; + if (nan(denominator)) return ieee ? denominator | 0x0008000000000000ull : denominator; + if ((zero(denominator) && zero(numerator)) || (infinite(denominator) && infinite(numerator))) return 0xfff8000000000000ull; + if (zero(denominator) || infinite(numerator)) return sign | Infinity; + if (infinite(denominator) || zero(numerator)) return sign; + if (exponent(numerator) - exponent(denominator) < -1075) return sign; + if (nan(quotient)) return sign | Infinity; + return sign | (quotient & Magnitude); +} + +void Check(const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sources = Sources(tid); + const std::uint64_t expected = Fixup(sources[0], sources[1], sources[2], mode.flushed, mode.ieee); + const std::uint64_t actual = Output[tid * Results] | (static_cast(Output[tid * Results + 1u]) << 32u); + Require(actual == expected, std::string("v_div_fixup_f64 ") + mode.name + " of " + Hex(sources[0]) + ", " + Hex(sources[1]) + ", " + Hex(sources[2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt, false, false}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true, true}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true, false}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false, true}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false, false}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true, true}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true, false}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false, true}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false, false}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_div_fixup_f64 denormal mode 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/DrawCacheRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp new file mode 100644 index 0000000000..b5650189c7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp @@ -0,0 +1,226 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array SecondVertexCode = VertexCode; +alignas(256) constexpr std::array PixelCode{0xf4000100u, 0xfa000000u, 0x7e000204u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array ForwardedPixelCode{ + 0xf4040080u, 0xfa000000u, 0xbf8cc07fu, 0xf4000101u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xd5480001u, + 0x02050800u, 0x7e020d01u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(16) std::array Other{0.5f, 0.0f, 0.0f, 0.0f}; +alignas(16) std::array ForwardedData{}; +alignas(32) std::array, 6> AlignedTables{}; +alignas(32) std::array OffsetTable{}; +alignas(256) std::array ColorMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; +alignas(8) Header SecondVertexHeader{}; +alignas(8) Header TriangleVertexHeader{}; +alignas(8) Header ForwardedPixelHeader{}; + +const Shader* Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); + return &header.shader; +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawFrom(std::initializer_list sources, const std::uint32_t* vertexCode = VertexCode.data(), const std::uint32_t* pixelCode = PixelCode.data()) { + const auto color = reinterpret_cast(ColorMemory.data()); + const auto vertex = reinterpret_cast(vertexCode); + const auto pixel = reinterpret_cast(pixelCode); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 2u << 1u}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + for (const auto* source : sources) { + const auto address = reinterpret_cast(source); + Append(commands, 0x76, {0x00c, static_cast(address)}); + Append(commands, 0x76, {0x00d, static_cast(address >> 32u)}); + Append(commands, 0x2d, {3, 2}); + } + return commands; +} + +void MovedDataHitsTheCache(bool libraries) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const auto draw = [&](std::initializer_list sources) { + auto commands = DrawFrom(sources); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + static std::array, 8> same{}; + for (auto& entry : same) entry = {1.0f, 0.0f, 0.0f, 0.0f}; + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (std::size_t i = 0; i < 4; ++i) counters = draw({same[i].data()}); + Require(counters.lookups == 4 && counters.hits == 0 && counters.inserts == 2, mode + ": the draws that teach the moved pointer and its contents made " + std::to_string(counters.hits) + " hits and " + std::to_string(counters.inserts) + " inserts, expected 0 and 2"); + for (std::size_t i = 4; i < same.size(); ++i) counters = draw({same[i].data()}); + Require(counters.lookups == 8 && counters.hits == 4 && counters.inserts == 2, mode + ": draws whose data moved again did not reuse the cached draw (" + std::to_string(counters.hits) + " hits in " + std::to_string(counters.lookups) + " lookups)"); + const auto other = draw({Other.data()}); + Require(other.lookups == 9 && other.hits == 4, mode + ": a draw whose moved data holds another value reused the cached draw"); + const auto again = draw({same[0].data()}); + Require(again.lookups == 10 && again.hits == 5, mode + ": a draw at an address seen before did not reuse the cached draw (" + std::to_string(again.hits) + " hits)"); +} + +void UnrelocatedAliasesStayOut(bool libraries) { + Registered(SecondVertexHeader, SecondVertexCode.data(), sizeof(SecondVertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const auto draw = [&](const float* source) { + auto commands = DrawFrom({source}, SecondVertexCode.data()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + alignas(16) static std::array repeated{1.0f, 0.0f, 0.0f, 0.0f}; + alignas(16) static std::array, 2>, 12> twice{}; + for (std::size_t i = 0; i < twice.size(); ++i) { + for (auto& entry : twice[i]) entry = {0.25f + 0.03125f * static_cast(i), 0.0f, 0.0f, 0.0f}; + } + draw(repeated.data()); + const auto start = draw(repeated.data()); + AgcDriver::DriverDetail::DrawEntryCounters counters = start; + for (const auto& pair : twice) { + for (const auto& entry : pair) draw(entry.data()); + counters = draw(repeated.data()); + } + const auto inserts = counters.inserts - start.inserts; + Require(inserts <= AgcDriver::DriverDetail::DrawRelocationTrial::ProbationAdmissions, mode + ": draws whose alias repeats but never relocates were cached (" + std::to_string(inserts) + " inserts for " + std::to_string(twice.size()) + " aliases)"); + Require(counters.hits - start.hits == twice.size(), mode + ": the exact key of a repeated draw lost its hits next to draws whose alias never relocates (" + std::to_string(counters.hits - start.hits) + " hits in " + std::to_string(twice.size()) + " draws)"); +} + +void MovedForwardedWordReachesStageDataHit(bool libraries) { + Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ForwardedPixelHeader, ForwardedPixelCode.data(), sizeof(ForwardedPixelCode), 1); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + constexpr std::byte Kept{0x40}; + const auto draw = [&](const void* table) { + ColorMemory.fill(Kept); + auto commands = DrawFrom({table}, TriangleVertexCode.data(), ForwardedPixelCode.data()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + const auto pixels = [&](std::array expected) { + std::size_t drawn = 0; + for (std::size_t at = 0; at < ColorMemory.size(); at += 4) { + const std::array pixel{static_cast(ColorMemory[at]), static_cast(ColorMemory[at + 1]), static_cast(ColorMemory[at + 2]), static_cast(ColorMemory[at + 3])}; + if (pixel == std::array{0x40, 0x40, 0x40, 0x40}) continue; + if (pixel != expected) return "pixel " + std::to_string(at / 4) + " is " + std::to_string(pixel[0]) + "," + std::to_string(pixel[1]) + "," + std::to_string(pixel[2]) + "," + std::to_string(pixel[3]) + ", expected " + std::to_string(expected[0]) + "," + std::to_string(expected[1]) + "," + std::to_string(expected[2]) + "," + std::to_string(expected[3]); + ++drawn; + } + return drawn == 0 ? std::string("the draw wrote no pixel") : std::string(); + }; + const auto data = static_cast(reinterpret_cast(ForwardedData.data())); + for (auto& table : AlignedTables) table[0] = data; + OffsetTable[2] = data; + ForwardedData = {1.0f, 0.0f, 0.0f, 0.0f}; + const auto start = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (const auto& table : AlignedTables) counters = draw(table.data()); + Require(counters.hits != start.hits, mode + ": draws whose pointer table moved to identical contents never reused the cached draw (" + std::to_string(counters.lookups - start.lookups) + " lookups, " + std::to_string(counters.inserts - start.inserts) + " inserts)"); + const auto taught = pixels({255, 0, 0, 255}); + Require(taught.empty(), mode + ": a draw through a pointer table with bit 4 of its address clear: " + taught); + ForwardedData[0] = 0.25f; + const auto before = counters; + counters = draw(&OffsetTable[2]); + Require(counters.lookups == before.lookups + 1 && counters.hits == before.hits + 1 && counters.stageDataHits == before.stageDataHits + 1, mode + ": a draw whose pointer table moved and whose data changed in place was not a relocated stage data hit (" + std::to_string(counters.hits - before.hits) + " hits, " + std::to_string(counters.stageDataHits - before.stageDataHits) + " stage data hits)"); + const auto moved = pixels({64, 255, 255, 64}); + Require(moved.empty(), mode + ": a relocated stage data hit did not draw with the moved user word: " + moved); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + MovedDataHitsTheCache(libraries); + UnrelocatedAliasesStayOut(libraries); + MovedForwardedWordReachesStageDataHit(libraries); + AgcDriverShutdown_nid_postfix(); + std::cout << "A draw whose data moved reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 516e85a915..ab9818c6ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "VulkanTestDevice.hpp" #include @@ -243,6 +244,7 @@ int main() { StageDataHitsRefreshTheirWords(libraries); RetainedPixelCacheTransitions(); AgcDriverShutdown_nid_postfix(); + Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "the driver's shutdown left the pipeline library worker running"); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp index 6754f0c84d..0fa8c4ebcd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp @@ -262,14 +262,15 @@ public: return result; } - AgcDriver::Graphics::State State() const { + AgcDriver::Graphics::State State(bool rectList = false) const { AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = rectList ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.rectList = rectList; state.viewport = {0.0f, static_cast(Height), static_cast(Width), -static_cast(Height), 0.0f, 1.0f}; state.scissor = {{0, 0}, {Width, Height}}; state.cullMode = VK_CULL_MODE_NONE; @@ -346,8 +347,8 @@ std::string PathName(const std::optional& path) { return path ? "the CPU path (" + std::string(AgcDriver::Graphics::IndirectDrawPathName(*path)) + ")" : std::string("the GPU"); } -std::string Classified(Fixture& fixture, const std::string& what, const ShaderRecompiler::RecompileResult& vertex, const std::vector& userData, AgcDriver::Pm4::DrawParameters& draw, std::optional expectedPath, Rule expectedRule) { - const auto path = AgcDriver::DriverDetail::Driver::ClassifyIndirectDraw(vertex, fixture.State(), Program(userData), fixture.device, draw, false); +std::string Classified(Fixture& fixture, const std::string& what, const ShaderRecompiler::RecompileResult& vertex, const std::vector& userData, AgcDriver::Pm4::DrawParameters& draw, std::optional expectedPath, Rule expectedRule, bool rectList = false) { + const auto path = AgcDriver::DriverDetail::Driver::ClassifyIndirectDraw(vertex, fixture.State(rectList), Program(userData), fixture.device, draw, false); const auto& indirect = *draw.indirect; const bool rules = expectedPath || (indirect.vertexRule == expectedRule && indirect.instanceRule == expectedRule); std::cout << what << ": classified to " << PathName(path) << '\n'; @@ -475,6 +476,15 @@ std::string Run(Fixture& fixture) { fixture.Draw(vertex, draw); failures += fixture.Check(what + ", drawn on the GPU", bufferIndices, 0x51u, 0x62u, true); } + { + const std::string what = "auto indirect rect-list record whose packet names no register for its offsets, user data s16 0x51, s17 0x62"; + const std::array record{Vertices, Instances, 300, 7}; + std::copy(record.begin(), record.end(), fixture.Records() + 32); + const auto vertex = fixture.Vertex(VertexCode, staleUserData); + AgcDriver::Pm4::DrawParameters draw{0, 0, 0, 0, 0, false, 0, 0}; + draw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast(fixture.Records() + 32), 0x24u, 16u, 16u, 1u, false, 0u, NoLocation, NoLocation, NoLocation, false, 0u}; + failures += Classified(fixture, what, vertex, staleUserData, draw, std::nullopt, Rule::Constant, true); + } } return failures; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 276ea252d6..1d96a16670 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -360,6 +360,7 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + const bool lodClamp = TargetHasCapability(device->Target(), spv::CapabilityMinLod); const auto* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); if (forced != nullptr && std::string_view(forced) == "1") Require((device->Target().srgbDecodeFormats & 3u) == 3u, "forced R8 and RG8 sRGB shader decoding is unavailable"); FillTexels(); @@ -408,7 +409,7 @@ int main() { } Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); - Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); + if (lodClamp) Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); Run(*device, {ClampEdge, FilterBilinear}, "bilinear, 2D array instruction on a 2D texture", false, false, true); Run(*device, {ClampEdge, FilterBilinear}, "bilinear, clamp to edge"); Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap"); @@ -417,12 +418,14 @@ int main() { Run(*device, {ClampBorder, FilterBilinear, BorderWhite}, "bilinear, white border"); Run(*device, {ClampBorder, FilterBilinear, BorderBlack}, "bilinear, black border"); Run(*device, {ClampEdge, FilterPoint, BorderBlack, 0u, 0x0140u}, "point, clamp to edge, sampler LOD bias"); - Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge, offsets, bias and LOD clamp", true); - Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap, offsets, bias and LOD clamp", true); - Run(*device, {ClampBorder, FilterBilinear, BorderWhite, 0u, 0x3f00u}, "bilinear, white border, offsets, bias and LOD clamp", true); + if (lodClamp) { + Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge, offsets, bias and LOD clamp", true); + Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap, offsets, bias and LOD clamp", true); + Run(*device, {ClampBorder, FilterBilinear, BorderWhite, 0u, 0x3f00u}, "bilinear, white border, offsets, bias and LOD clamp", true); + } for (unsigned iteration = 0; iteration < 2; ++iteration) { Run(*device, {ClampEdge, FilterPoint}, "cached point comparison", false, true); - Run(*device, {ClampWrap, FilterBilinear}, "cached bilinear comparison with offsets", true, true); + if (lodClamp) Run(*device, {ClampWrap, FilterBilinear}, "cached bilinear comparison with offsets", true, true); } Reject(*device, Format32Float, {ClampEdge, FilterPoint}, "native comparison with a nonconstant texel offset requires", OffsetCode, false); RunChannels(*device, QuadCode, {4, 5, 6, 7}, 4, 0, "view R,G,B,A"); @@ -443,7 +446,7 @@ int main() { Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); Reject(*device, Format8888UNorm, {ClampBorder, FilterPoint, BorderTable}, "border color table"); Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, ReductionMin}, "min or max reduction"); - std::puts("emulated color compare tests passed"); + std::puts(lodClamp ? "emulated color compare tests passed" : "emulated color compare tests passed, the offset and LOD clamp cases were skipped without shaderResourceMinLod"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp new file mode 100644 index 0000000000..86aa123c60 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp @@ -0,0 +1,193 @@ +#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 = 64; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t a; + std::uint32_t b; + std::uint32_t expected[16]; +}; + +struct Case { + const char* name; + Shader shader; + std::span rows; +}; + +constexpr Vector VCvtF16F32Rows[] = { + {0x7f800001u, 0x00000000u, {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u}}, + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33400000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33a00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33c00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x387ff000u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu}}, + {0x38800800u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, + {0x38801800u, 0x00000000u, {0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, + {0x7f800001u, 0x00000000u, {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u}}, + {0x80800000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0xb3400000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0xb3800000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0xb3a00000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0xb3c00000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0xb87ff000u, 0x00000000u, {0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008000u, 0x00008000u, 0x000083ffu, 0x000083ffu, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008000u, 0x00008000u, 0x000083ffu, 0x000083ffu}}, + {0xb8800800u, 0x00000000u, {0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u}}, + {0xb8801800u, 0x00000000u, {0x00008401u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u}}, +}; + +constexpr Vector VCvtF16F32ClampRows[] = { + {0x00800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33400000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33a00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33c00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x387ff000u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu}}, + {0x38800800u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, + {0x38801800u, 0x00000000u, {0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, +}; + +constexpr Vector VCvtF16U16Rows[] = { + {0x00000801u, 0x00000000u, {0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u}}, + {0x00000803u, 0x00000000u, {0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u}}, + {0x00008001u, 0x00000000u, {0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u}}, + {0x00008011u, 0x00000000u, {0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u}}, +}; + +constexpr Vector VCvtF16I16Rows[] = { + {0x00000801u, 0x00000000u, {0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u}}, + {0x00000803u, 0x00000000u, {0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u}}, + {0x00008001u, 0x00000000u, {0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu}}, + {0x00008009u, 0x00000000u, {0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu}}, +}; + +constexpr Vector VLdexpF16Rows[] = { + {0x00000001u, 0x0000ffe2u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u}}, + {0x00000001u, 0x0000000au, {0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u}}, + {0x00000001u, 0x000003e8u, {0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00007bffu, 0x00000000u, 0x00007bffu, 0x00000000u, 0x00007bffu, 0x00000000u, 0x00007bffu}}, + {0x00000003u, 0x0000fffeu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000003u, 0x0000ffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u}}, + {0x00000155u, 0x0000fff8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u}}, + {0x00000400u, 0x0000ffe2u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000400u, 0x0000fff6u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x00000400u, 0x0000001eu, {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu}}, + {0x00000401u, 0x0000fff5u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000401u, 0x0000fff6u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x00000555u, 0x0000fff7u, {0x00000000u, 0x00000000u, 0x00000003u, 0x00000003u, 0x00000000u, 0x00000000u, 0x00000003u, 0x00000003u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u}}, + {0x00008001u, 0x0000ffe2u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008001u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u}}, + {0x00008001u, 0x0000000au, {0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u}}, + {0x00008001u, 0x000003e8u, {0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fbffu, 0x00008000u, 0x0000fbffu, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fbffu, 0x00008000u, 0x0000fbffu}}, + {0x00008003u, 0x0000fffeu, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008003u, 0x0000ffffu, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u}}, + {0x00008155u, 0x0000fff8u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u}}, + {0x00008400u, 0x0000ffe2u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008400u, 0x0000fff6u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0x00008400u, 0x0000001eu, {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu}}, + {0x00008401u, 0x0000fff5u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008401u, 0x0000fff6u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0x00008555u, 0x0000fff7u, {0x00008000u, 0x00008000u, 0x00008003u, 0x00008003u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008003u, 0x00008003u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u}}, +}; + +const Case Cases[] = { + {"v_cvt_f16_f32", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e141504u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VCvtF16F32Rows}, + {"v_cvt_f16_f32 clamp", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd58a800au, 0x00000104u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u}}, VCvtF16F32ClampRows}, + {"v_cvt_f16_u16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e14a104u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VCvtF16U16Rows}, + {"v_cvt_f16_i16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e14a304u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VCvtF16I16Rows}, + {"v_ldexp_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x76140b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VLdexpF16Rows}, +}; + +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, const Case& item, std::uint32_t round16, std::uint32_t denorm16) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].a; + Input[row * Inputs + 1] = item.rows[row].b; + } + 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(item.shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{(round16 << 2u) | (denorm16 << 6u), true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t round16, std::uint32_t denorm16) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto& vector = item.rows[row]; + const auto expected = vector.expected[round16 * 4u + denorm16]; + Require(Output[row] == expected, std::string(item.name) + " round16 " + std::to_string(round16) + " denorm16 " + std::to_string(denorm16) + " inputs " + + Hex(vector.a) + " " + Hex(vector.b) + " is " + Hex(Output[row]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto& item : Cases) { + for (std::uint32_t round16 = 0u; round16 < 4u; ++round16) { + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + Run(*device, item, round16, denorm16); + Check(item, round16, denorm16); + } + } + } + std::puts("f16 conversion float mode 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/F16FmaDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp new file mode 100644 index 0000000000..0bf9bf31c7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp @@ -0,0 +1,251 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd74b000au, 0x041a0b04u, + 0x7e160306u, 0x6c160b04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[Threads][3] = { + {0x0001u, 0x0001u, 0x8400u}, + {0x7bffu, 0x7bffu, 0x0000u}, + {0x7c00u, 0x0000u, 0x0000u}, + {0x3bffu, 0x0400u, 0x0000u}, + {0x3c01u, 0x03ffu, 0x0000u}, + {0x0400u, 0x8400u, 0x0400u}, + {0x0400u, 0x0400u, 0x8400u}, + {0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x8000u}, + {0x0000u, 0x0000u, 0x0400u}, + {0x0000u, 0x0001u, 0x0001u}, + {0x0000u, 0x0001u, 0x03ffu}, + {0x0000u, 0x0001u, 0x0200u}, + {0x0000u, 0x7c00u, 0x0001u}, + {0x0000u, 0x7c00u, 0x8001u}, + {0x0000u, 0x7c00u, 0x03ffu}, + {0x0000u, 0x0001u, 0x0000u}, + {0x0000u, 0x0001u, 0x8000u}, + {0x0000u, 0x0001u, 0x0400u}, + {0x0001u, 0x8001u, 0x0000u}, + {0x0001u, 0x83ffu, 0x0000u}, + {0x0001u, 0x8400u, 0x0000u}, + {0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0200u}, + {0x0001u, 0x3800u, 0x8001u}, + {0x0001u, 0x3c00u, 0x03ffu}, + {0x0001u, 0xbc00u, 0x83ffu}, + {0x0400u, 0x3c00u, 0x0001u}, + {0x0400u, 0x3c00u, 0x03ffu}, + {0x0400u, 0x3c00u, 0x0200u}, + {0x0000u, 0x0001u, 0x8001u}, + {0x0000u, 0x0001u, 0x83ffu}, + {0x0000u, 0x03ffu, 0x8001u}, + {0x0000u, 0x0000u, 0x8001u}, + {0x0000u, 0x0000u, 0x83ffu}, + {0x0000u, 0x0400u, 0x8001u}, + {0x0400u, 0x3800u, 0x0000u}, + {0x0400u, 0x3800u, 0x8000u}, + {0x0400u, 0x3800u, 0x8400u}, + {0x0001u, 0x3c00u, 0x8400u}, + {0x0001u, 0xbc00u, 0x0000u}, + {0x0001u, 0xbc00u, 0x0400u}, + {0x0001u, 0x3c00u, 0x0000u}, + {0x0001u, 0x3c00u, 0x8000u}, + {0x0001u, 0xbc00u, 0x8000u}, + {0x0400u, 0x3800u, 0x0001u}, + {0x0400u, 0x3800u, 0x8001u}, + {0x0400u, 0xb800u, 0x0001u}, + {0x0400u, 0x3800u, 0x03ffu}, + {0x0400u, 0x3800u, 0x0200u}, + {0x0400u, 0xb800u, 0x83ffu}, + {0x0001u, 0xb800u, 0x0001u}, + {0x0001u, 0x3c00u, 0x8001u}, + {0x0001u, 0xbc00u, 0x0001u}, + {0x0400u, 0x3800u, 0x83ffu}, + {0x0400u, 0xb800u, 0x03ffu}, + {0x0400u, 0x1fffu, 0x83ffu}, + {0x0001u, 0x3800u, 0x8400u}, + {0x0001u, 0xb800u, 0x0400u}, + {0x8001u, 0x3800u, 0x0400u}, + {0x0400u, 0x3bffu, 0x83ffu}, + {0x8400u, 0x3bffu, 0x03ffu}, + {0x0401u, 0xb800u, 0x0200u}, +}; +constexpr std::uint32_t Expected[Threads][4] = { + {0x8400u, 0x8400u, 0x8400u, 0x8400u}, + {0x7c00u, 0x7c00u, 0x7c00u, 0x7c00u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0x0000u, 0x0000u, 0x0400u, 0x0400u}, + {0x0000u, 0x0400u, 0x0000u, 0x0400u}, + {0x0400u, 0x0400u, 0x0400u, 0x0400u}, + {0x8400u, 0x8400u, 0x8400u, 0x8400u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0400u, 0x0400u, 0x0400u, 0x0400u}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x0000u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0200u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0400u, 0x0400u, 0x0400u, 0x0400u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x0000u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0200u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x0400u, 0x0000u, 0x0400u}, + {0x8000u, 0x8400u, 0x8000u, 0x8400u}, + {0x0400u, 0x0401u, 0x0400u, 0x0401u}, + {0x0400u, 0x07ffu, 0x0400u, 0x07ffu}, + {0x0400u, 0x0600u, 0x0400u, 0x0600u}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x8000u, 0x0000u, 0x83ffu}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x8000u, 0x0000u, 0x83ffu}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x0000u, 0x0200u, 0x0200u}, + {0x0000u, 0x0000u, 0x0200u, 0x0200u}, + {0x8000u, 0x8000u, 0x8200u, 0x8200u}, + {0x8400u, 0x8000u, 0x8400u, 0x83ffu}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0400u, 0x0000u, 0x0400u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x8000u, 0x8000u, 0x8000u, 0x8001u}, + {0x0000u, 0x0000u, 0x0200u, 0x0201u}, + {0x0000u, 0x0000u, 0x0200u, 0x01ffu}, + {0x8000u, 0x8000u, 0x8200u, 0x81ffu}, + {0x0000u, 0x05ffu, 0x0200u, 0x05ffu}, + {0x0000u, 0x0400u, 0x0200u, 0x0400u}, + {0x8000u, 0x85ffu, 0x8200u, 0x85ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x8000u, 0x0200u, 0x81ffu}, + {0x8000u, 0x0000u, 0x8200u, 0x01ffu}, + {0x0000u, 0x8000u, 0x0008u, 0x83f7u}, + {0x8400u, 0x8000u, 0x8400u, 0x8400u}, + {0x0400u, 0x0000u, 0x0400u, 0x0400u}, + {0x0400u, 0x0000u, 0x0400u, 0x0400u}, + {0x0000u, 0x0000u, 0x0400u, 0x0000u}, + {0x8000u, 0x8000u, 0x8400u, 0x8000u}, + {0x8000u, 0x8000u, 0x8200u, 0x8000u}, +}; + +struct Mode { + const char* name; + std::optional value; + std::uint32_t column; +}; + +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%04x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +bool Matches(std::uint32_t actual, std::uint32_t expected) { + const auto nan = [](std::uint32_t half) { return (half & 0x7fffu) > 0x7c00u; }; + return actual == expected || (nan(expected) && actual <= 0xffffu && nan(actual)); +} + +void Check(const Mode& mode) { + constexpr const char* Names[Results] = {"v_fma_f16", "v_fmac_f16"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t expected = Expected[tid][mode.column]; + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt, 3u}, + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, 0u}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, 0u}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, 1u}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, 1u}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, 2u}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, 2u}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("f16 fma denormal mode 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/F16PackDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16PackDenormalModes.cpp new file mode 100644 index 0000000000..44554b31dc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16PackDenormalModes.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Threads = ValueCount * ValueCount; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x5e140b04u, 0xd711000bu, 0x00020f06u, + 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint32_t Singles[ValueCount] = { + 0x00000000u, 0x80000000u, 0x33800000u, 0xb3800000u, 0x33c00000u, 0x33000000u, 0x387fc000u, 0xb87fc000u, + 0x387fffffu, 0x38800000u, 0xb8800000u, 0x37730edfu, 0x3f800000u, 0x477ff000u, 0x7f800000u, 0xff800000u, +}; + +constexpr std::uint32_t SinglesTowardZero[ValueCount] = { + 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x0001u, 0x0000u, 0x03ffu, 0x83ffu, + 0x03ffu, 0x0400u, 0x8400u, 0x00f3u, 0x3c00u, 0x7bffu, 0x7c00u, 0xfc00u, +}; + +constexpr std::uint32_t Halves[ValueCount] = { + 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x83ffu, 0x0200u, 0x0400u, + 0x8400u, 0x3c00u, 0xbc00u, 0x7bffu, 0x7c00u, 0xfc00u, 0x7e00u, 0x7d00u, +}; + +struct Mode { + const char* name; + std::optional value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Fill() { + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + Input[pair * Inputs] = Singles[pair / ValueCount]; + Input[pair * Inputs + 1u] = Singles[pair % ValueCount]; + Input[pair * Inputs + 2u] = Halves[pair / ValueCount]; + Input[pair * Inputs + 3u] = Halves[pair % ValueCount]; + } +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Flush(std::uint32_t half, bool flushed) { + return flushed && (half & 0x7fffu) < 0x0400u ? half & 0x8000u : half; +} + +void Check(const Mode& mode) { + const std::uint32_t field = mode.value.has_value() ? mode.value->floatMode >> 6u : 3u; + const bool ieee = mode.value.has_value() && mode.value->ieeeMode; + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + const std::uint32_t lhs = pair / ValueCount; + const std::uint32_t rhs = pair % ValueCount; + const bool flushResults = (field & 2u) == 0u; + const std::uint32_t packed = Flush(SinglesTowardZero[lhs], flushResults) | (Flush(SinglesTowardZero[rhs], flushResults) << 16u); + Require(Output[pair * Results] == packed, std::string("v_cvt_pkrtz_f16_f32 ") + mode.name + " of " + Hex(Singles[lhs]) + " and " + Hex(Singles[rhs]) + " is " + Hex(Output[pair * Results]) + ", expected " + Hex(packed)); + const auto half = [&](std::uint32_t value) { + const std::uint32_t bits = Flush(value, (field & 1u) == 0u); + return ieee && (bits & 0x7fffu) > 0x7c00u ? bits | 0x0200u : bits; + }; + const std::uint32_t joined = half(Halves[lhs]) | (half(Halves[rhs]) << 16u); + Require(Output[pair * Results + 1u] == joined, std::string("v_pack_b32_f16 ") + mode.name + " of " + Hex(Halves[lhs]) + " and " + Hex(Halves[rhs]) + " is " + Hex(Output[pair * Results + 1u]) + ", expected " + Hex(joined)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Fill(); + const Mode modes[] = { + {"without a known float mode", std::nullopt}, + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("f16 pack denormal mode 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/Float16MinMaxBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp index 8c6b30f646..a70e3086fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp @@ -85,7 +85,6 @@ const std::array PackedChecks{{ struct Mode { const char* name; std::optional floatMode; - bool packed = true; }; const std::array Modes{{ @@ -94,8 +93,8 @@ const std::array Modes{{ {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, - {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, false}, - {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}, false}, + {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}}, }}; std::uint64_t Seed = 0x2545f4914f6cdd1dull; @@ -214,14 +213,7 @@ struct Model { bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu; if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u; } - std::uint32_t lane = Numeric(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits; - lane = IsNan(bits[0]) ? bits[1] : lane; - lane = IsNan(bits[1]) ? bits[0] : lane; - if (ieee) { - lane = IsSnan(bits[1]) ? Quiet(bits[1]) : lane; - lane = IsSnan(bits[0]) ? Quiet(bits[0]) : lane; - } - result |= Clamp(check.clamp, lane) << (16u * high); + result |= Clamp(check.clamp, Pair(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits) << (16u * high); } return result; } @@ -267,8 +259,7 @@ int main() { const bool flushInputs = mode.floatMode.has_value() && (mode.floatMode->floatMode & 0x40u) == 0u; const Model model{mode.floatMode.has_value() && mode.floatMode->ieeeMode, !mode.floatMode.has_value() || mode.floatMode->dx10Clamp, flushInputs}; const auto scalar = compile(ScalarCode, mode); - std::optional packed; - if (mode.packed) packed = compile(PackedCode, mode); + const auto packed = compile(PackedCode, mode); for (std::uint32_t batch = 0; batch < Batches; ++batch) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { std::uint32_t* row = &Input[tid * Inputs]; @@ -293,7 +284,7 @@ int main() { } }; run(scalar, ScalarCode, ScalarChecks, false); - if (packed) run(*packed, PackedCode, PackedChecks, true); + run(packed, PackedCode, PackedChecks, true); } } std::printf("f16 min/max tests passed (%llu results)\n", static_cast(compared)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp new file mode 100644 index 0000000000..959ceebe7b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Stride = 16; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0x64140b04, + 0x66160b04, + 0x68180b04, + 0x6a1a0b04, + 0xd74b000e, 0x041a0b04, + 0xd74b980f, 0x041a0b04, + 0xd74b3110, 0xc4160d04, + 0x7e220306, 0x6c220b04, + 0x6e240b04, 0x00003d55, + 0x70260b04, 0x00000001, + 0xd74ba214, 0x241a0b04, + 0x662a08f2, + 0xd5328116, 0x20020b04, + 0xd5350217, 0x40020d04, + 0xd5338018, 0x00020b04, + 0xd5340019, 0x20020b04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr const char* Names[Results] = { + "v_add_f16 v10, v4, v5", + "v_sub_f16 v11, v4, v5", + "v_subrev_f16 v12, v4, v5", + "v_mul_f16 v13, v4, v5", + "v_fma_f16 v14, v4, v5, v6", + "v_fma_f16 v15, v4, v5, v6 op_sel:[1,1,0,0] clamp", + "v_fma_f16 v16, |v4|, -v6, -v5 op_sel:[0,1,1,0]", + "v_fmac_f16 v17, v4, v5", + "v_fmamk_f16 v18, v4, 0x3d55, v5", + "v_fmaak_f16 v19, v4, v5, 0x0001", + "v_fma_f16 v20, -v4, |v5|, v6 op_sel:[0,0,1,0] clamp", + "v_sub_f16 v21, 1.0, v4", + "v_add_f16 v22, -|v4|, v5 clamp", + "v_mul_f16 v23, v4, -|v6|", + "v_sub_f16 v24, v4, v5 clamp", + "v_subrev_f16 v25, -v4, v5", +}; + +struct Mode { + const char* name; + std::optional floatMode; + bool native = true; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, true}}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10 ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, true}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, true}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, + {"f16 denormals flushed", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, false}, +}}; + +std::uint64_t Seed = 0x9e3779b97f4a7c15ull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 10u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | ((0x1du + (bits >> 20u) % 2u) << 10u) | mantissa; + case 5u: return sign | ((1u + (bits >> 20u) % 4u) << 10u) | mantissa; + case 6u: return sign | ((0x0du + (bits >> 20u) % 5u) << 10u) | mantissa; + default: return bits & 0xffffu; + } +} + +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; +} + +struct F16x2Math { + bool add = false; + bool mul = false; + bool fma = false; +}; + +F16x2Math FindF16x2Math(std::span spirv) { + std::optional half; + std::optional pair; + F16x2Math math; + for (std::size_t offset = 5u; offset < spirv.size();) { + const std::uint32_t words = spirv[offset] >> 16u; + const std::uint32_t opcode = spirv[offset] & 0xffffu; + Require(words != 0u && offset + words <= spirv.size(), "f16 native: malformed SPIR-V"); + if (opcode == spv::OpTypeFloat) { + Require(words >= 3u, "f16 native: malformed float type"); + if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + } + if (opcode == spv::OpTypeVector) { + Require(words >= 4u, "f16 native: malformed vector type"); + if (half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + } + if (opcode == spv::OpFAdd || opcode == spv::OpFMul || opcode == spv::OpExtInst) { + Require(words >= 5u, "f16 native: malformed arithmetic instruction"); + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } + } + offset += words; + } + return math; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + const auto supports = [&](spv::Capability capability) { + return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); + }; + if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { + std::puts("f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals"); + return VulkanTestSkipped; + } + std::vector withoutFloat16; + std::ranges::copy_if(target.supportedCapabilities, std::back_inserter(withoutFloat16), [](std::uint32_t capability) { return capability != spv::CapabilityFloat16; }); + auto emulatedTarget = target; + emulatedTarget.supportedCapabilities = withoutFloat16; + + const std::span code(Code); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](const ShaderRecompiler::SpirvTarget& shaderTarget, const Mode& mode) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + shaderTarget, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + const auto run = [&](const auto& result) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + return Output; + }; + + std::uint64_t compared = 0u; + std::uint64_t nanPayloads = 0u; + const auto isNan16 = [](std::uint32_t value) { return (value & 0x7fffu) > 0x7c00u; }; + for (const auto& mode : Modes) { + const auto native = compile(target, mode); + const auto emulated = compile(emulatedTarget, mode); + const F16x2Math nativeMath = FindF16x2Math(native.spirv.Words()); + const F16x2Math emulatedMath = FindF16x2Math(emulated.spirv.Words()); + Require(nativeMath.add == mode.native && nativeMath.mul == mode.native, std::string("f16 native: ") + mode.name + (mode.native ? " does not use" : " uses") + " f16vec2 add and mul"); + Require(!nativeMath.fma, std::string("f16 native: ") + mode.name + " uses an f16vec2 Fma, which Vulkan does not require to be fused"); + Require(!emulatedMath.add && !emulatedMath.mul && !emulatedMath.fma, std::string("f16 native: ") + mode.name + " uses f16vec2 math without Float16"); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (auto& word : Input) word = Half() | (Half() << 16u); + const auto nativeOutput = run(native); + const auto emulatedOutput = run(emulated); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const std::uint32_t actual = nativeOutput[tid * Stride + index]; + const std::uint32_t expected = emulatedOutput[tid * Stride + index]; + const std::uint32_t* row = &Input[tid * Inputs]; + const bool bothNan = (actual >> 16u) == (expected >> 16u) && isNan16(actual) && isNan16(expected); + if (actual != expected && bothNan) ++nanPayloads; + Require(actual == expected || bothNan, std::string("f16 native: ") + mode.name + ", " + Names[index] + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", f32 path gives " + Hex(expected)); + } + } + compared += Threads * Results; + } + } + std::printf("f16 native tests passed (%llu results, %llu NaN results with another payload)\n", static_cast(compared), static_cast(nanPayloads)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp new file mode 100644 index 0000000000..c772a521e1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp @@ -0,0 +1,221 @@ +#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 = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc20600au, 0x1c120b04u, 0xcc20600bu, + 0x0c120d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; +constexpr std::uint32_t Rows[Threads][3] = { + {0x00010000u, 0x00000000u, 0x3f800000u}, + {0x80010000u, 0x00000000u, 0x00800000u}, + {0x00003c00u, 0x00000001u, 0x4b000000u}, + {0x80000001u, 0x00003c00u, 0xbf800000u}, + {0x000003ffu, 0x00003c00u, 0x33800000u}, + {0x000183ffu, 0x00004000u, 0x00000001u}, + {0x00000200u, 0x00000200u, 0x3e800000u}, + {0x00013c00u, 0x00003c00u, 0x7f7fffffu}, + {0x00010400u, 0x00003800u, 0x3f800000u}, + {0x00007c00u, 0x00000001u, 0x3f800000u}, + {0x00010001u, 0x00000001u, 0x40000000u}, + {0x81554400u, 0x00000155u, 0xc0800000u}, + {0x83ff3c00u, 0x00000400u, 0x3f000000u}, + {0x00007bffu, 0x00000001u, 0x2f800000u}, + {0x80000000u, 0x00008000u, 0x80000000u}, + {0x00008001u, 0x00008001u, 0x3f800000u}, +}; +constexpr std::uint32_t Expected[2][2][Threads][2] = { + { + { + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x4b000000u}, + {0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38000000u, 0x38800000u}, + {0xffc00000u, 0x7f800000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0xc1800000u}, + {0x38800000u, 0x3f000000u}, + {0x00000000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + }, + { + {0x33800000u, 0x33800000u}, + {0xb3800000u, 0xb3800000u}, + {0x33800000u, 0x4b000000u}, + {0x33800000u, 0xb3800000u}, + {0x387fc000u, 0x2c7fc000u}, + {0xb8ffa000u, 0x33800000u}, + {0x30800000u, 0x37000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38004000u, 0x38802000u}, + {0x7f800000u, 0x7f800000u}, + {0x33800000u, 0x34400000u}, + {0x387fc000u, 0xc180000bu}, + {0x33800000u, 0x3efff802u}, + {0x3b7fe000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x27800000u, 0xb3800000u}, + }, + }, + { + { + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x4b000000u}, + {0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38000000u, 0x38800000u}, + {0xffc00000u, 0x7f800000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0xc1800000u}, + {0x38800000u, 0x3f000000u}, + {0x00000000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + }, + { + {0x33800000u, 0x33800000u}, + {0xb3800000u, 0xb3800000u}, + {0x33800000u, 0x4b000000u}, + {0x33800000u, 0xb3800000u}, + {0x387fc000u, 0x2c7fc000u}, + {0xb8ffa000u, 0x33800000u}, + {0x30800000u, 0x37000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38004000u, 0x38802000u}, + {0x7f800000u, 0x7f800000u}, + {0x33800000u, 0x34400000u}, + {0x387fc000u, 0xc180000bu}, + {0x33800000u, 0x3efff802u}, + {0x3b7fe000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x27800000u, 0xb3800000u}, + }, + }, +}; + +struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +bool Matches(std::uint32_t actual, std::uint32_t expected) { + const auto nan = [](std::uint32_t single) { return (single & 0x7fffffffu) > 0x7f800000u; }; + return actual == expected || (nan(expected) && nan(actual)); +} + +void Check(const Mode& mode) { + constexpr const char* Names[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"}; + const std::uint32_t f32Kept = (mode.value.floatMode >> 4u) & 1u; + const std::uint32_t f16InputsKept = (mode.value.floatMode >> 6u) & 1u; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + const std::uint32_t expected = Expected[f32Kept][f16InputsKept][tid][form]; + Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector modes; + static const char* const Names[2][4][2] = { + {{"f32 denormals flushed, f16 denormal mode 0, IEEE mode off", "f32 denormals flushed, f16 denormal mode 0, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 1, IEEE mode off", "f32 denormals flushed, f16 denormal mode 1, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 2, IEEE mode off", "f32 denormals flushed, f16 denormal mode 2, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 3, IEEE mode off", "f32 denormals flushed, f16 denormal mode 3, IEEE mode on"}}, + {{"f32 denormals kept, f16 denormal mode 0, IEEE mode off", "f32 denormals kept, f16 denormal mode 0, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 1, IEEE mode off", "f32 denormals kept, f16 denormal mode 1, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 2, IEEE mode off", "f32 denormals kept, f16 denormal mode 2, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 3, IEEE mode off", "f32 denormals kept, f16 denormal mode 3, IEEE mode on"}}, + }; + for (std::uint32_t f32 = 0; f32 < 2u; ++f32) { + for (std::uint32_t f16 = 0; f16 < 4u; ++f16) { + for (std::uint32_t ieee = 0; ieee < 2u; ++ieee) { + modes.push_back({Names[f32][f16][ieee], ShaderRecompiler::ShaderFloatMode{(f16 << 6u) | (f32 != 0u ? 0x30u : 0u), true, ieee != 0u, false}}); + } + } + } + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_fma_mix_f32 f16 denormal mode 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/GpuIndirectArgumentsMisalignedWriterAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp new file mode 100644 index 0000000000..12eb8b6c06 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp @@ -0,0 +1,123 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t ArgumentsOffset = 4; +std::array BlockStorage{}; +const std::span Block{reinterpret_cast((reinterpret_cast(BlockStorage.data()) + BlockBytes - 1) & ~(BlockBytes - 1)), BlockBytes}; +alignas(256) std::array WriterDescriptor{}; +alignas(256) std::array Counter{}; +alignas(256) std::array CounterDescriptor{}; +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}; +} + +std::uint32_t Argument(std::size_t index) { + std::uint32_t value = 0; + std::memcpy(&value, Block.data() + ArgumentsOffset + index * sizeof(value), sizeof(value)); + return value; +} + +void SetArguments(std::uint32_t x, std::uint32_t y, std::uint32_t z) { + const std::array values{x, y, z}; + std::memcpy(Block.data() + ArgumentsOffset, values.data(), sizeof(values)); +} + +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); +} + +} + +int main() { + try { + Require(reinterpret_cast(Block.data()) % BlockBytes == 0, "the indirect arguments block is not 64 KiB aligned"); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GuestAllocations::Mutation().Add(Block.data(), BlockBytes, true, true, true); + SetArguments(1, 1, 1); + const auto* arguments = Block.data() + ArgumentsOffset; + WriterDescriptor = Descriptor(arguments, 16); + CounterDescriptor = Descriptor(Counter.data(), sizeof(Counter)); + std::vector warmUp; + Program(warmUp, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + warmUp.insert(warmUp.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + 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 && Counter[0] == 1, "the warm-up dispatch did not run"); + Counter[0] = 0; + const auto adoptedBefore = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Program(words, WriterCode.data(), {Low(WriterDescriptor.data()), High(WriterDescriptor.data()), 2u, 3u, 4u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Program(words, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + words.insert(words.end(), {0xc0021600u, Low(arguments), High(arguments), 0x8041u}); + 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(Argument(0) == 2 && Argument(1) == 3 && Argument(2) == 4, "the first dispatch did not write the indirect arguments through its misaligned binding"); + Require(Counter[0] == 24, "an indirect dispatch whose arguments the dispatch before it writes through a misaligned binding ran " + std::to_string(Counter[0]) + " groups, expected 24"); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix() - adoptedBefore; + Require(adopted == 2, "the gated group adopted " + std::to_string(adopted) + " dispatches resolved at the gate, expected 2: a GPU-read indirect dispatch whose registered arguments an earlier dispatch writes through a misaligned binding must still be captured at the gate"); + GuestAllocations::Mutation().Remove(Block.data()); + std::cout << "gpu indirect arguments misaligned writer 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/GpuIndirectArgumentsWrittenAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp new file mode 100644 index 0000000000..7f0640e1fc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp @@ -0,0 +1,101 @@ +#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 WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +alignas(256) std::array Arguments{1, 1, 1, 0}; +alignas(256) std::array ArgumentsDescriptor{}; +alignas(256) std::array Counter{}; +alignas(256) std::array CounterDescriptor{}; +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); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + ArgumentsDescriptor = Descriptor(Arguments.data(), sizeof(Arguments)); + CounterDescriptor = Descriptor(Counter.data(), sizeof(Counter)); + std::vector warmUp; + Program(warmUp, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + warmUp.insert(warmUp.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + 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 && Counter[0] == 1, "the warm-up dispatch did not run"); + Counter[0] = 0; + const auto adoptedBefore = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Program(words, WriterCode.data(), {Low(ArgumentsDescriptor.data()), High(ArgumentsDescriptor.data()), 2u, 3u, 4u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Program(words, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + words.insert(words.end(), {0xc0021600u, Low(Arguments.data()), High(Arguments.data()), 0x8041u}); + 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(Arguments[0] == 2 && Arguments[1] == 3 && Arguments[2] == 4, "the first dispatch did not write the indirect arguments"); + Require(Counter[0] == 24, "an indirect dispatch whose arguments the dispatch before it writes ran " + std::to_string(Counter[0]) + " groups, expected 24"); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix() - adoptedBefore; + Require(adopted == 2, "the gated group adopted " + std::to_string(adopted) + " dispatches resolved at the gate, expected 2: a GPU-read indirect dispatch whose arguments an earlier dispatch writes must still be captured at the gate"); + std::cout << "gpu indirect arguments 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/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp index f415d1e5e8..aefdae5293 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -31,6 +31,7 @@ namespace { using AgcDriver::DriverDetail::Driver; using AgcDriver::Graphics::Require; +constexpr std::size_t HostStackContextBytes = 16 * 1024; constexpr std::size_t ContextBytes = 64 * 1024; constexpr std::size_t MappingBytes = 4 * 65536; constexpr std::size_t FiberStackLimit = 8 * 1024; @@ -84,10 +85,28 @@ void ReleaseMapping() { Mapping = nullptr; } -std::size_t FiberStackUsed() { +std::size_t FiberStackUsed(std::size_t contextBytes) { + const std::size_t words = contextBytes / sizeof(std::uint64_t); std::size_t untouched = 0; - while (untouched < FiberContext.size() && FiberContext[untouched] == Fill) ++untouched; - return (FiberContext.size() - untouched) * sizeof(std::uint64_t); + while (untouched < words && FiberContext[untouched] == Fill) ++untouched; + return (words - untouched) * sizeof(std::uint64_t); +} + +struct FiberMapping { + std::uint64_t threads = 0; + std::size_t used = 0; +}; + +FiberMapping MapOnFiberWithContext(std::size_t contextBytes) { + 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 before = Driver::GpuMapImportThreads(); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "GPU mapping on a fiber: the fiber did not run"); + const auto threads = Driver::GpuMapImportThreads() - before; + Require(sceFiberFinalize(fiber) == 0, "GPU mapping on a fiber: the fiber was not finalized"); + GuestAllocations::Mutation().Remove(Mapping); + return {threads, FiberStackUsed(contextBytes)}; } } @@ -103,29 +122,24 @@ int main() { #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 onHostStack = MapOnFiberWithContext(HostStackContextBytes); + const auto onContext = MapOnFiberWithContext(ContextBytes); 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"); + Require(onHostStack.used <= FiberStackLimit, "GPU mapping on a fiber with a host stack used " + std::to_string(onHostStack.used) + " bytes of the fiber's context, more than " + std::to_string(FiberStackLimit) + ": the import of the mapped memory ran on it"); + Require(onContext.used <= FiberStackLimit, "GPU mapping on a fiber running on its context used " + std::to_string(onContext.used) + " bytes of it, 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"); + Require(onHostStack.threads == 0, "GPU mapping on a fiber with a host stack started " + std::to_string(onHostStack.threads) + " 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"); + Require(onHostStack.threads == 1, "GPU mapping on a fiber with a host stack started " + std::to_string(onHostStack.threads) + " host threads for its import instead of one"); #endif + Require(onContext.threads == 1, "GPU mapping on a fiber running on its context started " + std::to_string(onContext.threads) + " host threads for its import instead of one"); 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"; + std::cout << "A GPU mapping made on a fiber used " << onHostStack.used << " bytes of its context with a host stack and " << onContext.used << " running on the context\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp index 465df1cabf..6e3186841c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp @@ -47,6 +47,18 @@ alignas(256) constexpr std::array ExplicitLodCode{ 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, }; +alignas(256) constexpr std::array ArrayExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayLevelZeroCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf09c0f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + alignas(256) constexpr std::array LineLodCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301008, 0x80000301, 0xbf8c3f70, 0xf0900f00, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, @@ -78,6 +90,7 @@ constexpr std::array SamplerCases{{ constexpr std::array LodU{0.03125f, 0.33125f, 0.96875f, 0.0f, 1.0f, -0.2f, 1.3f, 0.6f}; constexpr std::array Lods{0.0f, 1.0f, 2.0f, 1.25f, 2.75f, 3.6f, 6.0f, -1.0f}; +constexpr std::array Slices{0.0f, 1.0f, 2.7f, -1.0f, 5.0f}; constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; constexpr std::array LineY{0u, 1u, 2u, 3u, 0xffffffffu, 7u, 0u, 5u}; @@ -250,6 +263,24 @@ void FillLodInput() { } } +void FillArrayInput(bool levelZero) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lod = levelZero ? 0.0f : LaneLod(tid); + const std::array words{Bits(LaneU(tid)), Bits(LineV[(tid / 2u) % LineV.size()]), Bits(Slices[tid % Slices.size()]), Bits(lod)}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +void FillLineInput(bool levelZero) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lod = levelZero ? 0.0f : LaneLod(tid); + const std::array words{Bits(LaneU(tid)), 0u, Bits(lod), 0u}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + std::array SampleResults() { std::array results{}; for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy_n(&Buffer[tid * Words + SampleResult], 4u, &results[tid * 4u]); @@ -296,6 +327,28 @@ void CheckExplicitLod(AgcDriver::VulkanDevice& device) { } } } + +void CheckArray(AgcDriver::VulkanDevice& device) { + FillMippedLine(); + for (const bool levelZero : {false, true}) { + for (const auto& sampler : SamplerCases) { + const auto samplerWords = SamplerDescriptor(sampler); + FillLineInput(levelZero); + Run(device, LineLodCode, MippedLineDescriptor(), samplerWords); + const auto reference = SampleResults(); + FillArrayInput(levelZero); + Run(device, levelZero ? std::span(ArrayLevelZeroCode) : std::span(ArrayExplicitLodCode), MippedLineDescriptor(), samplerWords); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto what = std::string(levelZero ? "image_sample_lz" : "image_sample_l") + " 2d array on a 1D texture with " + sampler.name + ", u " + std::to_string(LaneU(tid)) + ", v " + std::to_string(LineV[(tid / 2u) % LineV.size()]) + ", slice " + std::to_string(Slices[tid % Slices.size()]); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = Buffer[tid * Words + SampleResult + component]; + const auto expected = reference[tid * 4u + component]; + Require(value == expected, what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " has bits " + std::to_string(value) + ", image_sample_l 1d returned bits " + std::to_string(expected)); + } + } + } + } +} } int main() { @@ -311,6 +364,7 @@ int main() { Run(*device, Code, PlaneDescriptor()); CheckPlane(); CheckExplicitLod(*device); + CheckArray(*device); std::puts("image plane address on line tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp index d0418ce365..a6536f8c9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -31,6 +31,8 @@ constexpr std::uint32_t ReductionMin = 1; constexpr std::uint32_t ReductionMax = 2; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t Aniso2x = 1; constexpr std::uint32_t MipNone = 0; constexpr std::uint32_t MipPoint = 1; constexpr std::uint32_t MipLinear = 2; @@ -67,6 +69,7 @@ struct Sampler { std::uint32_t reduction; std::uint32_t filter; std::uint32_t mip; + std::uint32_t anisoRatio = 0; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -89,7 +92,7 @@ std::array TextureDescriptor(std::uint32_t format, const void* } std::array SamplerDescriptor(const Sampler& sampler) { - return {0x92u | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u}; + return {0x92u | (sampler.anisoRatio << 9u) | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u}; } void FillTexels() { @@ -301,6 +304,8 @@ int main() { Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip"); Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip"); Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip"); + Run(*device, {ReductionMin, FilterAnisoBilinear, MipPoint, Aniso2x}, "min, 2x anisotropic bilinear, point mip"); + Run(*device, {ReductionMax, FilterAnisoBilinear, MipPoint, Aniso2x}, "max, 2x anisotropic bilinear, point mip"); Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min"); Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max"); RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp index a178c8c162..8daddabd90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp @@ -55,9 +55,15 @@ alignas(256) constexpr std::array LoadCode{ 0xbf810000, }; -alignas(256) constexpr std::array D16Code{0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array D16Code{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x8001141e, + 0x7e3e0281, 0xf0201508, 0x8001141e, 0xbf810000, +}; alignas(256) constexpr std::array PackedCode{0x7e3c0300, 0x7e3e0280, 0xf0281108, 0x0001141e, 0xbf810000}; -alignas(256) constexpr std::array AtomicD16Code{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array AtomicD16Code{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, + 0xf0201108, 0x8001141e, 0xbf810000, +}; alignas(256) constexpr std::array PackedWideCode{0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0281308, 0x0001141e, 0xbf810000}; alignas(256) constexpr std::array AtomicCode{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xbf810000}; @@ -181,13 +187,21 @@ std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint return Saturate(Value(tid, index), format.bits); } -std::vector Expected(const Format& format, std::span writes) { +std::uint32_t StoredD16Component(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + const auto half = (Value(tid, index / 2u) >> (16u * (index & 1u))) & 0xffffu; + return Saturate(SignExtend(half, 16u), format.bits); +} + +std::vector Expected(const Format& format, std::span writes, bool d16 = false) { auto bytes = Initial(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { for (const auto& write : writes) { auto* texel = bytes.data() + TexelOffset(format, tid, write.row); for (std::uint32_t component = 0; component < format.components; ++component) { - const auto value = StoredComponent(format, tid, write.dmask, component); + const auto value = d16 ? StoredD16Component(format, tid, write.dmask, component) : StoredComponent(format, tid, write.dmask, component); std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); } } @@ -244,8 +258,8 @@ void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span(Input.size() * 4u))); } -void CheckTexels(const std::uint8_t* texels, const Format& format, std::span writes, const std::string& what) { - const auto expected = Expected(format, writes); +void CheckTexels(const std::uint8_t* texels, const Format& format, std::span writes, const std::string& what, bool d16 = false) { + const auto expected = Expected(format, writes, d16); for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { const auto offset = TexelOffset(format, x, y); @@ -297,8 +311,10 @@ int main() { } Store(*device, texels, TitleCode, Formats[0].format, TitleSwizzle); CheckTexels(texels, Formats[0], TitleWrites, "image_store dmask:0x1 glc with DST_SEL X,1,1,1"); - RequireRefused(*device, texels, D16Code, 12u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 16_SINT"); - RequireRefused(*device, texels, D16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 32_SINT"); + for (const auto& format : Formats) { + Store(*device, texels, D16Code, format.format, SwizzleOf(format)); + CheckTexels(texels, format, StoreWrites, "image_store d16", true); + } RequireRefused(*device, texels, PackedCode, 12u, "storage image descriptor uses an unsupported format", "image_store_pck of 16_SINT"); RequireRefused(*device, texels, AtomicCode, 12u, "atomic image descriptor uses an unsupported format 12", "image_atomic_add of 16_SINT"); RequireRefused(*device, texels, StoreCode, 73u, "storage image descriptor uses an unsupported format", "image_store of 32_32_32_SINT"); @@ -306,7 +322,8 @@ int main() { Store(*device, texels, PackedWideCode, Formats[index].format, SwizzleOf(Formats[index])); CheckTexels(texels, Formats[index], PackedWideWrites, "image_store_pck dmask:0x3"); } - RequireRefused(*device, texels, AtomicD16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of an atomically updated 32_SINT image"); + Store(*device, texels, AtomicD16Code, Formats[2].format, IdentitySwizzle); + CheckTexels(texels, Formats[2], TitleWrites, "image_store d16 after image_atomic_add", true); std::puts("image store sint tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 8ab6eae2a9..bbabab005a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -198,6 +198,11 @@ alignas(256) constexpr std::array StoreTableCode{ 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0xf0208f08u, 0x00090401u, 0xbf810000u, }; +alignas(256) constexpr std::array StoreLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0208f08u, 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array StoreTableCutCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7da400ffu, 0x00001000u, 0xf0208f08u, 0x00090401u, @@ -390,7 +395,7 @@ Outcome Execute(AgcDriver::VulkanDevice& device, std::span outcome.cacheable = AgcDriver::DriverDetail::CacheableResult(result); outcome.imageTableReadRanges = result.imageTableReadRanges; for (const auto& binding : result.bindings) { - if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) outcome.imageTableCount = binding.count; + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) outcome.imageTableCount = binding.count; if (binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers) outcome.samplers = SamplerWords(binding.guestDescriptor); if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) outcome.samplerTable = SamplerWords(binding.guestDescriptor); } @@ -1047,15 +1052,37 @@ void RunModeTests(AgcDriver::VulkanDevice& device) { } void RunStoreTests(AgcDriver::VulkanDevice& device) { + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registeredUint(UintTexels.data(), sizeof(UintTexels)), registeredFloat(FloatTexels.data(), sizeof(FloatTexels)), registeredSint(SintTexels.data(), sizeof(SintTexels)); SetClassRecords(); - const auto saved = UintTexels; + const auto savedUint = UintTexels; + const auto savedFloat = FloatTexels; + const auto savedSint = SintTexels; + const auto untouched = [&] { return UintTexels == savedUint && FloatTexels == savedFloat && SintTexels == savedSint; }; Keys = {}; Keys[0] = 1; const auto cut = Run(device, StoreTableCutCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); Require(!Faulted(cut), "image table store: a store with EXEC clear faulted:\n" + cut.log); - const auto stored = Run(device, StoreTableCode, 1, Lanes, 64); - Require(Faulted(stored) && stored.log.find("unsupported operation") != std::string::npos, "image table store: an image_store through a table did not fault as an unsupported operation:\n" + stored.log); - Require(UintTexels == saved, "image table store: an unsupported store reached the image"); + Require(untouched(), "image table store: a store with EXEC clear reached an image"); + const auto stored = Run(device, StoreLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); + Require(!Faulted(stored), "image table store: a store through a table faulted:\n" + stored.log); + Require(stored.imageTableCount == 2u, "image table store: the storage table does not hold one element per distinct T# (" + std::to_string(stored.imageTableCount) + ")"); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == x, "image table store: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x)); + Require(std::equal(UintTexels.begin() + Width, UintTexels.end(), savedUint.begin() + Width), "image table store: a store to row 0 reached another row"); + Require(FloatTexels == savedFloat && SintTexels == savedSint, "image table store: a store through key 1 reached another entry's image"); + UintTexels = savedUint; + const auto restored = Run(device, StoreTableCutCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); + Require(!Faulted(restored) && untouched(), "image table store: restoring the image failed:\n" + restored.log); + Keys[0] = 6; + const auto null = Run(device, StoreLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); + Require(untouched(), "image table store: a store through a null entry reached an image:\n" + null.log); } void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp index 77dbe795c3..4d8dee9d60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -160,10 +160,14 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); const auto pixelResult = RecompilePixel(PixelCode, waveSize, Fp16Input, target, vertexPush); - const std::array shaders{{ - {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} - }}; + Require(pixelResult.barycentricEmulation.active != target.fragmentShaderBarycentricEnabled, "16-bit interpolation must emulate barycentrics exactly when the device lacks them"); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; @@ -259,6 +263,8 @@ ShaderRecompiler::SpirvTarget OfflineTarget(bool barycentric) { target.spirvVersion = 0x00010300u; target.subgroupSize = 64; target.fragmentShaderBarycentricEnabled = barycentric; + static constexpr std::array capabilities{spv::CapabilityFloat64}; + target.supportedCapabilities = capabilities; return target; } @@ -281,7 +287,8 @@ void ExpectRejected(std::uint32_t word0, std::uint32_t word1, const char* expect int main() { try { - ExpectFailure(PixelCode, Fp16Input, false, "requires fragmentShaderBarycentric", "16-bit interpolation was accepted without barycentrics"); + Require(RecompilePixel(PixelCode, 64u, Fp16Input, OfflineTarget(false), 0u).barycentricEmulation.active, "16-bit interpolation without barycentrics does not emulate them"); + Require(!RecompilePixel(PixelCode, 64u, Fp16Input, OfflineTarget(true), 0u).barycentricEmulation.active, "16-bit interpolation emulates barycentrics on a device that has them"); ExpectFailure(PixelCode, 0x03000000u, true, "without FP16_INTERP_MODE", "16-bit interpolation of a 32-bit input was accepted"); ExpectFailure(PixelCode, 0x03080420u, true, "passes its vertices through unchanged", "16-bit interpolation of a pass-through input was accepted"); ExpectRejected(0xd75a001bu, 0x0c6a0200u, "interpolation modifiers", "v_interp_p2_f16 accepted an output modifier"); @@ -291,12 +298,18 @@ int main() { ExpectRejected(0xd743001du, 0x003e0140u, "not a vector register", "v_interp_p1lv_f16 accepted a scalar P0"); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!device->Target().fragmentShaderBarycentricEnabled) { - std::puts("skipped, the device has no fragmentShaderBarycentric"); + if (!device->Target().fragmentShaderBarycentricEnabled && !device->GeometryLimits()) { + std::puts("skipped, the device has neither fragmentShaderBarycentric nor geometryShader"); return VulkanTestSkipped; } for (const auto waveSize : {64u, 32u}) { - Draw(*device, waveSize); + try { + Draw(*device, waveSize); + } catch (const std::exception& error) { + if (std::string(error.what()).find("need the Float64 capability") == std::string::npos) throw; + std::puts("skipped, the device has no Float64 capability for the exact interpolation sums"); + return VulkanTestSkipped; + } Check(waveSize); } std::puts("16-bit interpolation tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp new file mode 100644 index 0000000000..33aa721ef7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp @@ -0,0 +1,148 @@ +#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 = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd762000au, 0x02020b04u, 0xe0701000u, + 0x80010a03u, 0xbf810000u, +}; + +struct LdexpVector { + std::uint32_t value; + std::uint32_t exponent; + std::uint32_t outputsFlushed[4]; + std::uint32_t inputsFlushed[4]; +}; + +constexpr std::array Vectors{{ + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x807fffffu, 0x00000000u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, + {0x00000001u, 0x00000003u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x00000017u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x80000001u, 0x00000017u, {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, + {0x00000001u, 0x00000028u, {0x09000000u, 0x09000000u, 0x09000000u, 0x09000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x007fffffu, 0x00000001u, {0x00fffffeu, 0x00fffffeu, 0x00fffffeu, 0x00fffffeu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x007fffffu, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00400001u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000003u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00400000u, 0x00400000u, 0x00400000u}}, + {0x00800001u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00400001u, 0x00400000u, 0x00400000u}}, + {0x80800001u, 0xffffffffu, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80400000u, 0x80400000u, 0x80400001u, 0x80400000u}}, + {0x00c00000u, 0xffffffe8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0x80c00000u, 0xffffffe8u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000001u, 0x80000000u, 0x80000001u, 0x80000000u}}, + {0x00800000u, 0xffffffe8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0x00ffffffu, 0xffffffe9u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000002u, 0x00000002u, 0x00000001u, 0x00000001u}}, + {0x3f800000u, 0xffffff82u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}}, + {0x3f800000u, 0xffffff81u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00400000u, 0x00400000u, 0x00400000u}}, + {0x3fffffffu, 0xffffff81u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00800000u, 0x00800000u, 0x007fffffu, 0x007fffffu}}, + {0xbfffffffu, 0xffffff81u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80800000u, 0x807fffffu, 0x80800000u, 0x807fffffu}}, + {0x3f800000u, 0xffffff6bu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u}}, + {0x3f800000u, 0xffffff6au, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0xbf800000u, 0xffffff6au, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u}}, + {0x3fc00000u, 0xffffff6au, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0x3faaaaabu, 0xffffff7eu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x000aaaabu, 0x000aaaabu, 0x000aaaaau, 0x000aaaaau}}, + {0x7f7fffffu, 0xffffff03u, {0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu}, {0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu}}, + {0x7f7fffffu, 0xffffff02u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00800000u, 0x00800000u, 0x007fffffu, 0x007fffffu}}, + {0x007fffffu, 0x000003e8u, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3f800000u, 0x000000c8u, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}}, + {0xbf800000u, 0x000000c8u, {0xff800000u, 0xff7fffffu, 0xff800000u, 0xff7fffffu}, {0xff800000u, 0xff7fffffu, 0xff800000u, 0xff7fffffu}}, + {0x00000001u, 0x7fffffffu, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3f800000u, 0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0xbf800000u, 0x80000000u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u}}, + {0x00000000u, 0x00000005u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x80000000u, 0xfffffffbu, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, + {0x7f800000u, 0xfffffffbu, {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}}, + {0xff800000u, 0x00000005u, {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}, {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}}, + {0x7fc00000u, 0x00000003u, {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}}, +}}; + +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, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + Input[lane * Inputs] = Vectors[lane].value; + Input[lane * Inputs + 1] = Vectors[lane].exponent; + } + 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}, 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; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t denormals, std::uint32_t rounding, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + const auto& vector = Vectors[lane]; + const auto actual = Output[lane * Results]; + const auto expected = denormals == 1u ? vector.outputsFlushed[rounding] : vector.inputsFlushed[rounding]; + Require(actual == expected, "ldexp f32 denormal mode " + std::to_string(denormals) + ", round mode " + std::to_string(rounding) + (ieee ? ", IEEE=1" : ", IEEE=0") + + ": v_ldexp_f32 (" + Hex(vector.value) + ", " + Hex(vector.exponent) + ") is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t denormals : {1u, 2u}) { + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + for (const bool ieee : {false, true}) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u | rounding, true, ieee, false}); + Check(denormals, rounding, ieee); + } + } + } + std::puts("ldexp f32 denormal mode 1 and 2 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/LdsMaskWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp new file mode 100644 index 0000000000..eb4f2499b6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp @@ -0,0 +1,140 @@ +#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 Lanes = 64; +constexpr std::uint32_t Slots = 16; +constexpr std::uint32_t Stride = 4; +constexpr std::uint32_t Guard = 4096; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array WaterfallCode{ + 0x34240083, 0x7e040280, 0x7e060280, 0xd9340000, 0x00000212, 0xbf8cc07f, 0x16080085, 0x3608088f, + 0x34080883, 0xd6ff0002, 0x00010300, 0xd9280000, 0x00000204, 0xbf8cc07f, 0xd9d80000, 0x06000012, + 0xbf8cc07f, 0xd4e50002, 0x00020c80, 0x7e140280, 0x7e160280, 0xbe8a0380, 0xbefe0400, 0xbf138002, + 0xbf840016, 0xbe8b1402, 0xbe821c0b, 0x8f0c830b, 0x7e18020c, 0xd9d80000, 0x0e00000c, 0xbf8cc07f, + 0x7dca1c80, 0xbf86fff5, 0x7e1c050e, 0x7e1e050f, 0xbe90140e, 0x8f921081, 0x3a1c1c12, 0x3a1e1e13, + 0x4a141481, 0x4a161610, 0x800a810a, 0xbf08ff0a, 0x00001000, 0xbf850001, 0xbf82fff1, 0xbefe04c1, + 0x7e1a020a, 0xe0702000, 0x80010a00, 0xe0702004, 0x80010b00, 0xe0702008, 0x80010d00, 0xe070200c, + 0x80010600, 0xbf810000, +}; + +alignas(256) constexpr std::array MaskedOrCode{ + 0x34240083, 0x7e040280, 0x7e060280, 0xd9340000, 0x00000212, 0xbf8cc07f, 0x3608008f, 0x34080883, + 0xd6ff0002, 0x00010300, 0xbefe0400, 0xd9280000, 0x00000204, 0xbf8cc07f, 0xbefe04c1, 0xd9d80000, + 0x06000012, 0xbf8cc07f, 0xe0702000, 0x80010600, 0xe0702004, 0x80010700, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t records) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | ((Stride * 4u) << 16u), records, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t exec, bool spanned) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + userData[0] = static_cast(exec); + userData[1] = static_cast(exec >> 32u); + const auto output = BufferDescriptor(Output.data(), Lanes); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const auto address = reinterpret_cast(code.data()); + const std::array memory{{{address, std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, Lanes * 2u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, address, code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.target.spannedWaves = spanned; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Require(result.fullSubgroups == spanned, spanned ? "the program did not span two host subgroups" : "the program spans host subgroups without APS5_SPANNED_WAVE"); + device.Dispatch(result, 1, 1, 1, {}, address); + device.WaitIdle(); +} + +void Check(std::uint64_t exec, bool spanned) { + std::array slots{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) slots[(lane * 5u) & (Slots - 1u)] |= std::uint64_t{1} << lane; + std::uint32_t iterations = 0; + std::uint32_t indexSum = 0; + for (std::uint32_t slot = 0; slot < Slots; ++slot) { + for (auto mask = slots[slot]; mask != 0; mask &= mask - 1) { + ++iterations; + indexSum += static_cast(std::countr_zero(mask)); + } + } + char name[64]; + std::snprintf(name, sizeof(name), "%s, exec 0x%016llx", spanned ? "spanned" : "two-lane", static_cast(exec)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const bool active = ((exec >> lane) & 1u) != 0; + const std::array expected{active ? iterations : 0u, active ? indexSum : 0u, iterations, static_cast(slots[lane])}; + const std::array fields{"inner iterations", "bit index sum", "wave iterations", "own slot low word"}; + for (std::uint32_t field = 0; field < Stride; ++field) { + const auto actual = Output[lane * Stride + field]; + Require(actual != Guard + 1u || field != 2u, std::string(name) + ": the waterfall ran past " + std::to_string(Guard) + " iterations"); + Require(actual == expected[field], std::string(name) + ": lane " + std::to_string(lane) + " " + fields[field] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[field])); + } + } +} + +void CheckMaskedOr(std::uint64_t exec, bool spanned) { + std::array slots{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + if (((exec >> lane) & 1u) != 0) slots[lane & (Slots - 1u)] |= std::uint64_t{1} << lane; + } + char name[64]; + std::snprintf(name, sizeof(name), "%s masked ds_or_b64, exec 0x%016llx", spanned ? "spanned" : "two-lane", static_cast(exec)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const std::array expected{static_cast(slots[lane]), static_cast(slots[lane] >> 32u)}; + for (std::uint32_t word = 0; word < expected.size(); ++word) { + const auto actual = Output[lane * Stride + word]; + Require(actual == expected[word], std::string(name) + ": lane " + std::to_string(lane) + " slot word " + std::to_string(word) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[word])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + const bool spannable = device->Target().subgroupSize == 32u && device->ComputeFullSubgroups(); + if (!spannable) std::puts("spanned mode skipped: the device has no full 32-lane compute subgroups"); + for (const bool spanned : {false, true}) { + if (spanned && !spannable) continue; + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0xaaaaaaaaaaaaaaaa}, std::uint64_t{0xffff000000000000}, std::uint64_t{0x000000000000ffff}, std::uint64_t{1} << 63u, std::uint64_t{1}, std::uint64_t{0x0000000100000000}}) { + Run(*device, WaterfallCode, exec, spanned); + Check(exec, spanned); + } + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0x00000000ffffffff}, std::uint64_t{0xffffffff00000000}, std::uint64_t{1} << 40u, std::uint64_t{0x8000000000000001}}) { + Run(*device, MaskedOrCode, exec, spanned); + CheckMaskedOr(exec, spanned); + } + } + std::printf("LDS mask waterfall tests passed (two-lane%s)\n", spannable ? " and spanned" : ""); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp new file mode 100644 index 0000000000..993358e4c7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using 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; +constexpr std::uint32_t NoInput = 0xffffffffu; + +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, 4> 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 fixedVgpr, std::uint32_t stippleVgpr) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | (fixedVgpr << 9u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | (fixedVgpr << 9u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = stippleVgpr == NoInput ? 0x7e000280u | (22u << 17u) : 0x7e000c00u | (22u << 17u) | (256u + stippleVgpr); + 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, 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.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); +} + + +void Check(const std::vector& stored, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{static_cast(x), static_cast(y), 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]) + ")"); + } + } +} + +bool Expect(AgcDriver::VulkanDevice& device) { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), bytes) == nullptr) return false; + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == bytes, "the target is not a linear R32G32B32A32 target"); + Draw(device, color, PixelInputBit(PixelInput::PositionFixedPoint), Program(0u, NoInput)); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && !recorder->Idle(), "the draw into an imported linear target waited for the GPU instead of being recorded"); + device.WaitIdle(); + std::vector stored(bytes); + std::memcpy(stored.data(), block.data, bytes); + Check(stored, "after the recorded draw, raw guest read"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) { + std::puts("linear target resident test skipped: no host import on this device"); + return VulkanTestSkipped; + } + std::puts("linear target resident 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/MeshDrawRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshDrawRelocation.cpp new file mode 100644 index 0000000000..1b64d9d3df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshDrawRelocation.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "../MeshGeometryFixture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +alignas(256) std::array Pixels{}; +alignas(256) constexpr std::array Indices{0, 1, 2, 0}; +alignas(256) constexpr std::array, 3> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1} +}}; +alignas(256) constexpr std::array PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + +constexpr std::array LoadBufferDescriptor{0xf4080204u, 0xfa000000u, 0xbf8cc07fu}; +alignas(256) constexpr auto GeometryCode = [] { + std::array code{}; + std::copy(LoadBufferDescriptor.begin(), LoadBufferDescriptor.end(), code.begin()); + std::copy(AgcDriver::Test::MeshGeometryCode.begin(), AgcDriver::Test::MeshGeometryCode.end(), code.begin() + LoadBufferDescriptor.size()); + return code; +}(); + +alignas(16) std::array, 8> Descriptors{}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(header); + header.shader.shader_size = static_cast(size); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.users; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Write(std::vector& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); +} + +std::vector Setup(const std::array& descriptor) { + const auto color = reinterpret_cast(Pixels.data()); + const auto geometry = reinterpret_cast(GeometryCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto table = reinterpret_cast(descriptor.data()); + const std::vector> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, + {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, + {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, + {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(32.0f)}, {0x112, std::bit_cast(32.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + const std::vector> shaders{ + {0xc8, static_cast(geometry >> 8u)}, {0xc9, static_cast(geometry >> 40u)}, + {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (2u << 1u)}, + {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, + {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} + }; + std::vector commands; + Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); + for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); + for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); + commands.insert(commands.end(), {0xc0002a00u, 0u}); + return commands; +} + +AgcDriver::DriverDetail::DrawEntryCounters Draw(const std::array& descriptor) { + Pixels.fill(std::byte{0}); + auto commands = Setup(descriptor); + const auto address = reinterpret_cast(Indices.data()); + commands.insert(commands.end(), {0xc0042700u, 3u, static_cast(address), static_cast(address >> 32u), 3u, 0u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(std::to_integer(Pixels[(24u * Width + 16u) * 4u]) > 200u, "the mesh draw did not cover its triangle"); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +void MovedDescriptorTableHitsTheCache() { + const auto vertices = reinterpret_cast(Vertices.data()); + for (auto& descriptor : Descriptors) descriptor = {static_cast(vertices), static_cast(vertices >> 32u) | (32u << 16u), static_cast(Vertices.size()), 0x01016facu}; + const auto front = static_cast(DrawMiss::FrontDiffering); + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (std::size_t i = 0; i < 4; ++i) counters = Draw(Descriptors[i]); + Require(counters.lookups == 4 && counters.hits == 0, "the draws that teach the moved table made " + std::to_string(counters.hits) + " hits in " + std::to_string(counters.lookups) + " lookups, expected 0 in 4"); + const auto learned = counters; + for (std::size_t i = 4; i < Descriptors.size(); ++i) counters = Draw(Descriptors[i]); + Require(counters.misses[front] == learned.misses[front], "a relocated mesh stage was refused as a differing front stage (" + std::to_string(counters.misses[front] - learned.misses[front]) + " times)"); + Require(counters.hits - learned.hits == Descriptors.size() - 4, "mesh draws whose table moved again did not reuse the cached draw (" + std::to_string(counters.hits - learned.hits) + " hits in " + std::to_string(Descriptors.size() - 4) + " draws)"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + } + Register(GeometryHeader, GeometryCode.data(), sizeof(GeometryCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + MovedDescriptorTableHitsTheCache(); + AgcDriverShutdown_nid_postfix(); + std::cout << "A mesh draw whose descriptor table moved reused the cached draw\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp new file mode 100644 index 0000000000..c449f905e8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp @@ -0,0 +1,150 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "../MeshGeometryFixture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +alignas(256) std::array Pixels{}; +alignas(256) constexpr std::array Indices{0, 1, 2, 3, 4, 5, 0, 0, 0, 0}; +alignas(256) constexpr std::array RightIndices{3, 4, 5, 0}; +alignas(256) constexpr std::array, 6> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1}, + {0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.5f, 0.8f, 0, 1, 1, 1, 1, 1} +}}; +alignas(256) constexpr std::array PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(header); + header.shader.shader_size = static_cast(size); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.users; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Write(std::vector& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); +} + +std::vector Setup() { + const auto color = reinterpret_cast(Pixels.data()); + const auto geometry = reinterpret_cast(AgcDriver::Test::MeshGeometryCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto vertices = reinterpret_cast(Vertices.data()); + const std::vector> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, + {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, + {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, + {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(32.0f)}, {0x112, std::bit_cast(32.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + const std::vector> shaders{ + {0xc8, static_cast(geometry >> 8u)}, {0xc9, static_cast(geometry >> 40u)}, + {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (4u << 1u)}, + {0x8c, static_cast(vertices)}, {0x8d, static_cast(vertices >> 32u) | (32u << 16u)}, + {0x8e, static_cast(Vertices.size())}, {0x8f, 0x01016facu}, + {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} + }; + std::vector commands; + Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); + for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); + for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); + commands.insert(commands.end(), {0xc0002a00u, 0u}); + return commands; +} + +void Draw(const std::uint16_t* indices, std::uint32_t count, bool left, bool right) { + Pixels.fill(std::byte{0}); + auto commands = Setup(); + const auto address = reinterpret_cast(indices); + commands.insert(commands.end(), {0xc0042700u, count, static_cast(address), static_cast(address >> 32u), count, 0u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto lit = [](std::uint32_t x) { return std::to_integer(Pixels[(24u * Width + x) * 4u]) > 200u; }; + Require(lit(16) == left && lit(48) == right, "cached mesh draw used another draw's indices: count=" + std::to_string(count)); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + } + Register(GeometryHeader, AgcDriver::Test::MeshGeometryCode.data(), sizeof(AgcDriver::Test::MeshGeometryCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + for (unsigned i = 0; i < 3; ++i) Draw(Indices.data(), 3, true, false); + const auto warm = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(warm.hits != 0, "mesh warm-up did not reach the draw cache"); + const auto front = static_cast(DrawMiss::FrontDiffering); + const auto layout = static_cast(DrawMiss::Layout); + Draw(Indices.data(), 6, true, true); + const auto range = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(range.misses[front] == warm.misses[front] + 1 && range.misses[layout] == warm.misses[layout], "an index-range change was not counted as a differing front stage"); + Draw(Indices.data(), 6, true, true); + const auto rangeReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(rangeReuse.hits == range.hits + 1, "the changed index range was not retained for cache reuse"); + Draw(RightIndices.data(), 3, false, true); + const auto address = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(address.misses[front] == rangeReuse.misses[front] + 1 && address.misses[layout] == rangeReuse.misses[layout], "an index-address change was not counted as a differing front stage"); + Draw(RightIndices.data(), 3, false, true); + const auto addressReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(addressReuse.hits == address.hits + 1, "the changed index address was not retained for cache reuse"); + Draw(Indices.data(), 3, true, false); + const auto earlier = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(earlier.hits == addressReuse.hits + 1, "returning to an earlier index descriptor did not hit the cache"); + AgcDriverShutdown_nid_postfix(); + std::cout << "Cached mesh draws retained their index ranges and addresses\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp new file mode 100644 index 0000000000..f64c0541dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ZeroHighCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x36140cff, 0x0000ffff, 0xd705000a, 0x00020b04, 0xe0702000, 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array ChainCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x7e140280, 0xd703000a, 0x00020906, 0xd705000a, 0x00020b0a, 0xe0702000, 0x80010a03, + 0xbf810000, +}; + +alignas(256) constexpr std::array KeptHighCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x7e140306, 0xd705000a, 0x00020b04, 0xe0702000, 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array MaskedFactorsCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x360808ff, 0x0000ffff, 0x360a0aff, 0x0000ffff, 0x36140cff, 0x0000ffff, 0xd705000a, + 0x00020b04, 0xe0702000, 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedLowLaneCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x360808ff, 0x0000ffff, 0x360a0aff, 0x0000ffff, 0xcc01400a, 0x18020b04, 0xe0702000, + 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedHighLaneCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x360808ff, 0xffff0000, 0x360a0aff, 0xffff0000, 0xcc01400a, 0x18020b04, 0xe0702000, + 0x80010a03, 0xbf810000, +}; + +constexpr std::array, 8> Edges{{ + {0xffffffffu, 0xffffffffu, 0x00000000u}, {0x0000ffffu, 0x0000ffffu, 0xabcd1234u}, {0xffff0000u, 0xffff0000u, 0xffffffffu}, + {0x8000ffffu, 0x7fff8000u, 0x12345678u}, {0x12345678u, 0x9abcdef0u, 0x80000000u}, {0x00010001u, 0x00010001u, 0x0000ffffu}, + {0x0000ff00u, 0x00000100u, 0xffff0000u}, {0x00000000u, 0xffffffffu, 0x7fff7fffu}, +}}; + +void FillInput() { + std::uint64_t state = 0x9e3779b97f4a7c15ull; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* words = &Input[tid * Inputs]; + for (std::uint32_t j = 0; j < 3u; ++j) { + state = state * 6364136223846793005ull + 1442695040888963407ull; + words[j] = tid < Edges.size() ? Edges[tid][j] : static_cast(state >> 32u); + } + } +} + +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, std::span code) { + 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::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(); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Low16Product(std::uint32_t a, std::uint32_t b) { + return ((a & 0xffffu) * (b & 0xffffu)) & 0xffffu; +} + +void Check(const char* name, const std::function& expected) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto* in = &Input[tid * Inputs]; + const auto want = expected(in[0], in[1], in[2]); + Require(Output[tid] == want, std::string("mul lo u16: ") + name + ": thread " + std::to_string(tid) + " (" + Hex(in[0]) + ", " + Hex(in[1]) + ", " + Hex(in[2]) + ") wrote " + Hex(Output[tid]) + ", expected " + Hex(want)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device, ZeroHighCode); + Check("v_mul_lo_u16 into a register whose high half is zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); }); + Run(*device, ChainCode); + Check("v_add_nc_u16 then v_mul_lo_u16 in a cleared register", [](std::uint32_t a, std::uint32_t b, std::uint32_t c) { return Low16Product(c + a, b); }); + Run(*device, KeptHighCode); + Check("v_mul_lo_u16 into a register holding another value", [](std::uint32_t a, std::uint32_t b, std::uint32_t c) { return (c & 0xffff0000u) | Low16Product(a, b); }); + Run(*device, MaskedFactorsCode); + Check("v_mul_lo_u16 of factors masked to 16 bits into a register whose high half is zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); }); + Run(*device, PackedLowLaneCode); + Check("v_pk_mul_lo_u16 of factors whose high halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); }); + Run(*device, PackedHighLaneCode); + Check("v_pk_mul_lo_u16 of factors whose low halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a >> 16u, b >> 16u) << 16u; }); + std::puts("mul lo u16 high half 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/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp new file mode 100644 index 0000000000..d1564e3562 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -0,0 +1,186 @@ +#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/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.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 + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context Context(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +namespace Memory = AgcDriver::GuestMemory; + +constexpr std::size_t BlockBytes = 1048576; + +class GuestBlock { +public: + GuestBlock() { + Require(Memory::WriteWatched(), "native depth BDA clear requires guest write watching"); +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, BlockBytes)); + Require(block != nullptr, "cannot allocate the native depth BDA clear block"); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot create the native depth BDA clear backing"); + GuestArena::GuestArenaMapDirect_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); +#else + void* raw = mmap(nullptr, 2 * BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the native depth BDA clear block"); + 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 != begin + BlockBytes) munmap(reinterpret_cast(aligned + BlockBytes), begin + BlockBytes - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); +#endif + Require(Memory::Watched(Address(), BlockBytes), "the native depth BDA clear block is not watched"); + std::memset(block, 0xcd, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return reinterpret_cast(block); } + const std::uint8_t* Data() const { return block; } + void FillWith(std::uint8_t value) const { std::memset(block, value, BlockBytes); } + +private: + std::uint8_t* block = nullptr; +#ifdef _WIN32 + HANDLE section = nullptr; +#endif +}; + +int RunNativeClear(AgcDriver::VulkanDevice& device) { + using namespace ShaderRecompiler; + using namespace AgcDriver::Graphics; + const std::lock_guard lock(Memory::GpuMutex()); + const GuestBlock depth, metadata; + metadata.FillWith(0); + constexpr VkExtent2D extent{5u, 3u}; + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + alignas(256) static constexpr std::array code{ + 0xd7460000, 0x04010c06, 0x34000084, 0xdc388000, 0x04020000, + 0xbf8c3f70, 0xd7710008, 0x00120805, 0xd7710009, 0x00120a05, + 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, + 0xdc788000, 0x00020800, 0xbf810000, + }; + std::array userData{0u, 0u, static_cast(metadata.Address()), static_cast(metadata.Address() >> 32u), 0u, 0u}; + const ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + const auto groups = static_cast(metadataBytes / 1024u); + const std::array packet{0u, groups, 1u, 1u, 1u}; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == metadataBytes, "the native GPU clear kernel is not recognized"); + const auto compile = [&] { + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64u, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + auto result = Recompile(request); + Require(std::ranges::any_of(result.bindings, [](const auto& binding) { return binding.role == DescriptorRole::BdaPagetable; }), "the native clear shader did not use BDA"); + return result; + }; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + Require(std::all_of(metadata.Data(), metadata.Data() + BlockBytes, [](auto value) { return value == 0u; }), "the warm-up native clear did not retain zero metadata"); + if (!Memory::Watched(depth.Address(), BlockBytes) || !Memory::Watched(metadata.Address(), BlockBytes)) { + std::puts("skipped, the device does not retain guest write watching for native clear imports"); + return VulkanTestSkipped; + } + const auto context = AgcDriver::VulkanDeviceTestAccess::Context(device); + Require(context.recorder != nullptr, "the native clear context has no recorder"); + context.recorder->Activate(); + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{device}; + DepthTarget target{depth.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, 4u}; + target.depthSwizzle = 24u; + target.htileDepthOnly = target.iterate256 = target.htilePipeAligned = true; + const auto view = DepthSurfaceView(context, target); + const auto base = target.address >> 8u; + const auto htile = target.htileAddress >> 8u; + const std::array words{ + static_cast(base), static_cast((base >> 32u) & 255u) | (22u << 20u), + 1u | (2u << 14u), (14u << 28u) | (24u << 20u) | (2u << 16u) | 0x204u, 0u, 2u << 4u, + (1u << 10u) | (1u << 19u) | (1u << 21u) | (static_cast(htile & 255u) << 24u), static_cast(htile >> 8u), + }; + const auto sampled = CachedSampledTexture(context, words); + Require(sampled != nullptr, "the native GPU clear has no initial sampled view"); + device.WaitIdle(); + target.clearDepth = 1.0f; + Require(DepthSurfaceView(context, target) == view, "changing the native clear value replaced its attachment"); + NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes); + const auto generation = Memory::TrackerGeneration(); + userData[4] = 0xfffffff0u; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) { + std::uint32_t value = 0; + std::memcpy(&value, metadata.Data() + offset, 4u); + Require(value == (offset < metadataBytes ? 0xfffffff0u : 0u), "the native GPU clear did not write its exact metadata range"); + } + std::printf("Native full BDA clear: exact %zu-byte metadata readback passed; complete=%d\n", metadataBytes, Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation)); + Require(CachedSampledTexture(context, words) == sampled, "a completed full BDA clear lost its native sampled view"); + Require(DepthSurfaceView(context, target) == view, "a completed full BDA clear replaced its native attachment"); + device.WaitIdle(); + Require(std::all_of(depth.Data(), depth.Data() + BlockBytes, [](auto value) { return value == 0xcdu; }), "the native GPU clear changed guest depth bytes"); + std::puts("Native full BDA clear accepted"); + return 0; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + return RunNativeClear(*device); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp index d5f6b199f5..b643f84060 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp @@ -196,7 +196,7 @@ constexpr const char* Names[48] = { }; bool Ieee = false; -bool FlushHalf = false; +bool DirectedHalf = false; void Fill(std::uint32_t tid, std::uint32_t* words) { std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); @@ -236,7 +236,7 @@ void Run(AgcDriver::VulkanDevice& device) { }; request.useCache = false; if (Ieee) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; - if (FlushHalf) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}; + if (DirectedHalf) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf4u, true, false, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -264,14 +264,14 @@ int main() { Check(); } Ieee = false; - FlushHalf = true; + DirectedHalf = true; bool refused = false; try { Run(*device); } catch (const std::exception& error) { - refused = std::string(error.what()).find("f16 denormals not kept") != std::string::npos; + refused = std::string(error.what()).find("f16 rounding not to nearest even") != std::string::npos; } - Require(refused, "packed alu: flushed f16 denormals were not refused"); + Require(refused, "packed alu: directed f16 rounding was not refused"); std::puts("packed alu tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp new file mode 100644 index 0000000000..b4570abb67 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp @@ -0,0 +1,203 @@ +#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 = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040084u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc0f400au, 0x18020b04u, 0xcc10400bu, + 0x18020b04u, 0xcc0e400cu, 0x1c1a0b04u, 0xe07c1000u, 0x80010a02u, 0xbf810000u, +}; +constexpr std::uint32_t Rows[Threads][3] = { + {0x02000001u, 0x02000000u, 0x02000000u}, + {0x83ff8001u, 0x03ff0001u, 0x00000000u}, + {0x7c0003ffu, 0x00010001u, 0x00000000u}, + {0x7bff0400u, 0x3c008001u, 0x00010000u}, + {0x04013bffu, 0xb8000400u, 0x02000000u}, + {0xbc003c01u, 0x000103ffu, 0x04000000u}, + {0x3c000400u, 0x84010400u, 0x04008400u}, + {0x04000400u, 0x3bff8400u, 0x83ff0400u}, + {0x84000001u, 0x3bff0001u, 0x03ff8400u}, + {0x35553c00u, 0x05550001u, 0x00000000u}, + {0x01553800u, 0x44000001u, 0x00000001u}, + {0x81553800u, 0x44000400u, 0x04000000u}, + {0x00008400u, 0x80003800u, 0x80000000u}, + {0x80003c00u, 0x80003c00u, 0x00000001u}, + {0x5bff1fffu, 0x04002001u, 0x00000000u}, + {0x3bff2001u, 0x3bff1fffu, 0x00018001u}, +}; +constexpr std::uint32_t Expected[4][Threads][3] = { + { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80008000u, 0x00000000u}, + {0x7c000000u, 0xfe000000u, 0xfe000000u}, + {0x7bff0400u, 0x7bff8000u, 0x7bff0000u}, + {0xb8003bffu, 0x80000000u, 0x80000000u}, + {0xbc003c01u, 0x80000000u, 0x04000000u}, + {0x3c000800u, 0x84010000u, 0x80008400u}, + {0x3bff0000u, 0x00008000u, 0x00000400u}, + {0x3bff0000u, 0x80000000u, 0x80008400u}, + {0x35553c00u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x80000000u, 0x04000000u}, + {0x00003800u, 0x80008000u, 0x80008000u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe0400u}, + }, + { + {0x04000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80008000u, 0x80008000u}, + {0x7c000400u, 0x7c000000u, 0x7c000000u}, + {0x7bff0000u, 0x7bff8000u, 0x7bff8000u}, + {0xb8003bffu, 0x80000000u, 0x80000000u}, + {0xbc003c01u, 0x80000400u, 0x00000400u}, + {0x3c000800u, 0x84010000u, 0x80008400u}, + {0x3bff0000u, 0x00008000u, 0x00000400u}, + {0x3bff0000u, 0x80000000u, 0x80008400u}, + {0x35553c00u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x05540000u, 0x05540000u}, + {0x44003800u, 0x85540000u, 0x80000000u}, + {0x00003800u, 0x80008000u, 0x80008000u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe0000u}, + }, + { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80008000u, 0x00000000u}, + {0x7c000000u, 0xfe000000u, 0xfe000000u}, + {0x7bff0400u, 0x7bff8000u, 0x7bff0000u}, + {0xb8003bffu, 0x82000400u, 0x82000400u}, + {0xbc003c01u, 0x80000000u, 0x04000000u}, + {0x3c000800u, 0x84010000u, 0x80018400u}, + {0x3bff0000u, 0x04008000u, 0x04000400u}, + {0x3bff0000u, 0x84000000u, 0x84008400u}, + {0x35553c00u, 0x01c70000u, 0x01c70000u}, + {0x44003800u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x80000200u, 0x04000200u}, + {0x00003800u, 0x80008200u, 0x80008200u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe0400u}, + }, + { + {0x04000001u, 0x00000000u, 0x02000000u}, + {0x00000000u, 0x80008000u, 0x80008000u}, + {0x7c000400u, 0x7c000000u, 0x7c000000u}, + {0x7bff03ffu, 0x7bff8000u, 0x7bff8000u}, + {0xb8003bffu, 0x82000400u, 0x80000400u}, + {0xbc003c01u, 0x80010400u, 0x03ff0400u}, + {0x3c000800u, 0x84010000u, 0x80018400u}, + {0x3bff0000u, 0x04008000u, 0x00000400u}, + {0x3bff0002u, 0x84000000u, 0x80008400u}, + {0x35553c00u, 0x01c70001u, 0x01c70001u}, + {0x44003800u, 0x05540000u, 0x05540002u}, + {0x44003800u, 0x85540200u, 0x81540200u}, + {0x00003800u, 0x80008200u, 0x80008200u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe03ffu}, + }, +}; + +struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode value; + std::uint32_t field; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Mode& mode) { + constexpr const char* Names[3] = {"v_pk_add_f16", "v_pk_mul_f16", "v_pk_fma_f16"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < 3u; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + const std::uint32_t expected = Expected[mode.field][tid][form]; + Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, 0u}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, 0u}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, 1u}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, 1u}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, 2u}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, 2u}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("packed f16 denormal mode 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/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp new file mode 100644 index 0000000000..16d3742761 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp @@ -0,0 +1,431 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "../NarrowConstantStoreFixture.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color1555Info = 0x8044u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 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 PixelProgram{}; +std::size_t pixelProgramWords = 0; + +std::span Program(const std::array& color) { + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + PixelProgram[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + PixelProgram[at++] = std::bit_cast(color[channel]); + } + PixelProgram[at++] = 0xf800180fu; + PixelProgram[at++] = 0x07060504u; + PixelProgram[at++] = 0xbf810000u; + pixelProgramWords = at; + return std::span(PixelProgram.data(), at); +} + +class Block { +public: + 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 guest block"); + 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 guest block"); + 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) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Color1555Info; + cx[0x31d] = 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); + Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.bytes == block.bytes, "the test target did not decode as an A1R5G5B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +void ExpectTexel(AgcDriver::VulkanDevice& device, const Block& block, std::uint16_t texel, const char* what) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + const ColorTargetLayout layout(Width, Height, ColorTileMode::Linear, 2); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == texel) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x", x, y, stored[index], texel); + throw std::runtime_error(std::string(what) + detail); + } + } +} + +// The constant-store compute program with its two stored literals replaced: `salt` 0 is the +// fixture's program, every other value a program of its own with its own module. +std::vector ComputeCode(std::uint32_t salt) { + std::vector code(NarrowConstantStoreFixture::Code.begin(), NarrowConstantStoreFixture::Code.end()); + if (salt != 0) { + code[3] = 0x00a10200u ^ (salt * 0x01010101u); + code[5] = 0x0000fec3u ^ (salt * 0x00010001u); + } + return code; +} + +ShaderRecompiler::RecompileResult CompileCompute(AgcDriver::VulkanDevice& device, const std::vector& code, std::span userData, std::uint32_t waveSize) { + using namespace ShaderRecompiler; + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderComputeStageInfo compute{{NarrowConstantStoreFixture::Threads, 1, 1}, 0, {false, false, false}, false, 1, {}}; + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + return Recompile(request); +} + +void ExpectConstantStore(const Block& block, const char* what) { + using namespace NarrowConstantStoreFixture; + for (std::size_t offset = 0; offset < Threads * LaneBytes; ++offset) { + const auto expected = ExpectedLane[offset % LaneBytes]; + Require(block.data[offset] == expected, std::string(what) + ": byte " + std::to_string(offset) + " is " + std::to_string(block.data[offset]) + ", expected " + std::to_string(expected)); + } +} + +std::array AddressWords(const Block& block) { + const auto address = block.Address(); + return {static_cast(address), static_cast(address >> 32u)}; +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +struct Totals { + AgcDriver::Graphics::PipelineJournalCounters journal; + AgcDriver::Graphics::PipelineStoreCounters store; +}; + +Totals Snapshot() { + return {AgcDriver::Graphics::PipelineJournalTotals(), AgcDriver::Graphics::PipelineStoreTotals()}; +} + +void JournalTests() { + using namespace NarrowConstantStoreFixture; + Require(AgcDriver::Graphics::PipelineJournalEnabled(), "the pipeline journal is disabled although the shader cache directory is set"); + const auto journalPath = AgcDriver::Graphics::PipelineJournalPath(); + Block compute(BlockBytes); + Block target(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + const auto computeCode = ComputeCode(0); + const std::array red{1.0f, 0.0f, 0.0f, 1.0f}; + constexpr std::uint16_t redTexel = 0x801f; + { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "the first device is required once the second opened"); + device->WaitForPipelineJournalReplay(); + const auto before = Snapshot(); + Require(before.journal.loadedRecipes == 0 && before.journal.replayedGraphics == 0 && before.journal.replayedCompute == 0, "an empty journal replayed pipelines"); + std::memset(compute.data, Fill, compute.bytes); + const auto words = AddressWords(compute); + const auto result = CompileCompute(*device, computeCode, words, 32); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(computeCode.data())); + device->WaitIdle(); + ExpectConstantStore(compute, "first run: the constant store dispatch"); + std::memset(target.data, 0, target.bytes); + Draw(*device, DecodeTarget(target), Program(red)); + ExpectTexel(*device, target, redTexel, "first run: the draw"); + Require(device->ComputePipelinesCreated() == 1, "the first run did not build exactly one compute pipeline"); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto after = Snapshot(); + Require(after.store.misses == before.store.misses + 1 && after.store.hits == before.store.hits, "the first draw did not miss the pipeline store exactly once"); + Require(after.journal.recordedGraphics == 1 && after.journal.recordedCompute == 1 && after.journal.recordedModules == 3 && after.journal.writeFailures == 0, "the first run did not record one graphics and one compute pipeline with three modules"); + Draw(*device, DecodeTarget(target), Program(red)); + device->WaitIdle(); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto repeated = Snapshot(); + Require(repeated.store.hits == after.store.hits + 1 && repeated.store.misses == after.store.misses, "a repeated draw did not hit the pipeline store"); + Require(repeated.journal.recordedGraphics == 1 && repeated.journal.recordedModules == 3, "a repeated draw recorded its pipeline again"); + } + Require(std::filesystem::exists(journalPath) && std::filesystem::file_size(journalPath) > 0, "the journal file was not written"); + { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "the second device could not be opened"); + device->WaitForPipelineJournalReplay(); + const auto replayed = Snapshot(); + Require(replayed.journal.loadedRecipes == 2 && replayed.journal.loadedModules == 3 && replayed.journal.droppedRecords == 0, "the second run did not load the two recorded pipelines and their three modules"); + Require(replayed.journal.replayedGraphics == 1 && replayed.journal.replayedCompute == 1 && replayed.journal.replayRejected == 0 && replayed.journal.replayFailed == 0 && replayed.journal.replayPresent == 0, "the replay did not build one graphics and one compute pipeline"); + Require(device->ComputePipelinesCreated() == 1, "the replay did not build the compute pipeline on the new device"); + std::memset(compute.data, Fill, compute.bytes); + const auto words = AddressWords(compute); + const auto result = CompileCompute(*device, computeCode, words, 32); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(computeCode.data())); + device->WaitIdle(); + ExpectConstantStore(compute, "second run: the constant store dispatch"); + Require(device->ComputePipelinesCreated() == 1, "the second run's dispatch built a compute pipeline the replay had built"); + std::memset(target.data, 0, target.bytes); + Draw(*device, DecodeTarget(target), Program(red)); + ExpectTexel(*device, target, redTexel, "second run: the draw through the replayed pipeline"); + const auto drawn = Snapshot(); + Require(drawn.store.hits == replayed.store.hits + 1 && drawn.store.misses == replayed.store.misses, "the second run's draw did not find the replayed pipeline"); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto final = Snapshot(); + Require(final.journal.recordedGraphics == 1 && final.journal.recordedCompute == 1 && final.journal.recordedModules == 3, "the second run recorded pipelines the journal already held"); + } +} + +// --benchmark [count]: `count` distinct compute programs, dispatched for the first time on three +// devices in turn. The first device starts without the driver's pipeline cache file (a driver with +// an on-disk shader cache of its own, lavapipe's Mesa cache here, still serves from it unless that +// is disabled too, MESA_SHADER_CACHE_DISABLE=true, where the driver's compile dominates); it leaves +// the driver's cache and the journal warm. The second replays the journal before its dispatches. +// The third dispatches with the driver's cache warm but the journal file removed: what a warm run +// pays at each first use without the journal. Wall times only; no pass threshold. +void Benchmark(std::uint32_t count) { + using namespace NarrowConstantStoreFixture; + Require(AgcDriver::Graphics::PipelineJournalEnabled(), "the pipeline journal is disabled"); + Block compute(BlockBytes); + const auto words = AddressWords(compute); + std::vector> codes; + for (std::uint32_t salt = 1; salt <= count; ++salt) codes.push_back(ComputeCode(salt)); + struct Run { + double openMs = 0; + double dispatchMs = 0; + std::uint64_t created = 0; + AgcDriver::Graphics::PipelineJournalCounters journal; + }; + const auto run = [&](const char* what) { + Run measured; + const auto openedAt = std::chrono::steady_clock::now(); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, std::string("no device for the benchmark's ") + what); + device->WaitForPipelineJournalReplay(); + measured.openMs = std::chrono::duration(std::chrono::steady_clock::now() - openedAt).count(); + measured.journal = AgcDriver::Graphics::PipelineJournalTotals(); + std::vector results; + for (const auto& code : codes) results.push_back(CompileCompute(*device, code, words, 32)); + const auto createdBefore = device->ComputePipelinesCreated(); + const auto started = std::chrono::steady_clock::now(); + for (std::size_t index = 0; index < results.size(); ++index) device->Dispatch(results[index], 1, 1, 1, {}, reinterpret_cast(codes[index].data())); + device->WaitIdle(); + measured.dispatchMs = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + measured.created = device->ComputePipelinesCreated() - createdBefore; + AgcDriver::Graphics::FlushPipelineJournal(); + return measured; + }; + const auto cold = run("cold run"); + const auto replayed = run("replayed run"); + std::error_code error; + std::filesystem::remove(AgcDriver::Graphics::PipelineJournalPath(), error); + Require(!error, "cannot remove the journal for the benchmark's last run"); + const auto warm = run("warm run without the journal"); + std::printf("pipeline journal benchmark: %u compute programs, %u replay threads\n", count, replayed.journal.replayThreads); + std::printf(" cold driver cache, no journal: %llu pipelines built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(cold.created), count, cold.dispatchMs, cold.dispatchMs / count); + std::printf(" warm driver cache, no journal: %llu pipelines built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(warm.created), count, warm.dispatchMs, warm.dispatchMs / count); + std::printf(" warm driver cache, journal: %llu pipelines built by the replay in %.1f ms of replay work (%.1f ms from the device opening), then %llu built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(replayed.journal.replayedCompute), replayed.journal.replayMs, replayed.openMs, static_cast(replayed.created), count, replayed.dispatchMs, replayed.dispatchMs / count); +} + +} + +int main(int argc, char** argv) { + try { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-test-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(directory); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + SetEnvironment("APS5_NO_PIPELINE_JOURNAL", "0"); + SetEnvironment("APS5_VERIFY_SET_LAYOUTS", "1"); + SetEnvironment("APS5_PIPELINE_JOURNAL_THREADS", "2"); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (argc >= 2 && std::string(argv[1]) == "--benchmark") { + Benchmark(argc >= 3 ? static_cast(std::strtoul(argv[2], nullptr, 10)) : 24u); + return 0; + } + JournalTests(); + std::puts("pipeline journal record and replay passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp index e4e66f07f3..f2b56411c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -268,6 +268,9 @@ int main(int argc, char** argv) { OptimizationDrain drain{context.device}; std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + if (optimize) Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 1, "the optimized pipeline was not built on the pipeline library worker"); + AgcDriver::Graphics::StopPipelineLibraryWorker(); + Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "stopping the pipeline library left its worker running"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp new file mode 100644 index 0000000000..8cc143e3c5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::DrawPipeline; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::uint32_t DispatchOpcode = 0x15; + +constexpr std::size_t BlockBytes = 65536; + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + if (block == nullptr) throw std::runtime_error("pipelined dispatch: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, false); + return block; +} + +std::uint32_t* Input = nullptr; +std::uint32_t* Output = nullptr; +std::uint32_t* Scratch = nullptr; +std::uint32_t* Table = nullptr; +std::uint32_t* TargetA = nullptr; +std::uint32_t* TargetB = nullptr; + +alignas(256) constexpr std::array Code{0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +alignas(256) constexpr std::array ScalarCode{0xf4000200u, 0xfa000000u, 0xbf8cc07fu, 0x34020082u, 0x7e040208u, 0x4a040481u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +alignas(256) constexpr std::array TableCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80020201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined dispatch: " + what); +} + +template +void WaitUntil(Ready ready, const std::string& what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("pipelined dispatch: timed out waiting for " + what); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::array FillPacket(std::uint32_t value, const void* destination, std::uint32_t bytes) { + const auto address = reinterpret_cast(destination); + return {0xc0055000u, 2u << 29u, value, 0u, static_cast(address), static_cast(address >> 32u), bytes}; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::vector Round(const void* destination, std::uint32_t value, const void* code) { + const auto fill = FillPacket(value, destination, BufferBytes); + std::vector words(fill.begin(), fill.end()); + const auto codeAddress = reinterpret_cast(code); + const auto input = BufferDescriptor(Input, BufferBytes); + const auto output = BufferDescriptor(Output, BufferBytes); + const std::vector commands{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, input[0], input[1], input[2], input[3], output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + words.insert(words.end(), commands.begin(), commands.end()); + return words; +} + +std::vector TableFills(const void* target, std::uint32_t value) { + const auto targetFill = FillPacket(value, target, BufferBytes); + const auto tableFill = FillPacket(static_cast(reinterpret_cast(target)), Table, 4u); + std::vector words(targetFill.begin(), targetFill.end()); + words.insert(words.end(), tableFill.begin(), tableFill.end()); + return words; +} + +std::vector TableDispatch(const void* code) { + const auto codeAddress = reinterpret_cast(code); + const auto tableAddress = reinterpret_cast(Table); + const auto output = BufferDescriptor(Output, BufferBytes); + const std::vector commands{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u, output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + return commands; +} + +void Submit(std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + { + const auto probe = OpenVulkanTestDevice(); + if (!probe) return VulkanTestSkipped; + } + const char* dispatchSwitch = std::getenv("APS5_PIPELINE_DISPATCH"); + const bool pipelinedDispatch = dispatchSwitch != nullptr && dispatchSwitch[0] != '\0' && dispatchSwitch[0] != '0'; + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(Code); + shader.code = Code.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + Shader scalarShader{}; + scalarShader.file_header = 0x34333231; + scalarShader.version = 0x18; + scalarShader.header_size = sizeof(Shader); + scalarShader.shader_size = sizeof(ScalarCode); + scalarShader.code = ScalarCode.data(); + AgcDriverRegisterShader_nid_postfix(&scalarShader); + Shader tableShader{}; + tableShader.file_header = 0x34333231; + tableShader.version = 0x18; + tableShader.header_size = sizeof(Shader); + tableShader.shader_size = sizeof(TableCode); + tableShader.code = TableCode.data(); + AgcDriverRegisterShader_nid_postfix(&tableShader); + + Input = NewBlock(); + Output = NewBlock(); + Scratch = NewBlock(); + Table = NewBlock(); + TargetA = NewBlock(); + TargetB = NewBlock(); + check((reinterpret_cast(TargetA) >> 32u) == (reinterpret_cast(TargetB) >> 32u), "the two targets do not share their high address dword"); + const auto initialDescriptor = BufferDescriptor(TargetA, BufferBytes); + std::copy(initialDescriptor.begin(), initialDescriptor.end(), Table); + const std::array values{0x0a0b0c0du, 0x11112222u, 0x33334444u, 0x55556666u, 0x77778888u}; + for (std::size_t round = 0; round < values.size(); ++round) { + const auto value = values[round]; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Input, value, Code.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Input[lane] == value, "the immediate fill did not land in lane " + std::to_string(lane)); + check(Output[lane] == value + 1u, "round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (the dispatch did not see the fill before it)"); + } + std::printf("pipelined dispatch: input fill round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains <= 1, "a dispatch drained the pipeline more than once (" + std::to_string(drains) + ")"); + } + for (std::size_t round = 0; round < 3; ++round) { + const auto value = 0xc0de0000u + static_cast(round); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Scratch, value, Code.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Scratch), BufferBytes); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Scratch[lane] == value, "the scratch fill did not land in lane " + std::to_string(lane)); + check(Output[lane] == Input[lane] + 1u, "scratch round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(Input[lane] + 1u)); + } + std::printf("pipelined dispatch: scratch fill round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains == 0, "a dispatch that reads nothing pending drained the pipeline (" + std::to_string(drains) + ")"); + } + const std::array tableValues{0x6a6a0001u, 0x6b6b0002u, 0x6c6c0003u, 0x6d6d0004u}; + for (std::size_t round = 0; round < tableValues.size(); ++round) { + auto* target = round % 2 == 0 ? TargetA : TargetB; + const auto value = tableValues[round]; + std::atomic released{false}; + struct Release { + std::atomic& flag; + ~Release() { flag.store(true, std::memory_order_release); } + } release{released}; + WaitUntil([] { return DrawPipeline::QueuedItems() == 0; }, "the pipeline to empty before descriptor round " + std::to_string(round)); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + DrawPipeline::Queue0().Enqueue([&released] { + while (!released.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {}); + auto fills = TableFills(target, value); + Submit(fills); + WaitUntil([] { return DrawPipeline::QueuedItems() >= 4; }, "the target and table fills queued behind the gate"); + auto dispatch = TableDispatch(TableCode.data()); + Submit(dispatch); + WaitUntil([&drainsBefore] { return DrawPipeline::QueuedItems() >= 5 || DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) > drainsBefore; }, "the dispatch handled while the table fill is pending"); + released.store(true, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Output[lane] == value + 1u, "descriptor round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (the dispatch read the buffer the table named before the pending fill of the table)"); + } + std::printf("pipelined dispatch: descriptor round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains == 1, "a dispatch whose table entry is pending a write drained " + std::to_string(drains) + " times, expected once"); + } + const std::array scalarValues{0x13572468u, 0x2468ace0u, 0x0f1e2d3cu, 0x9abcdef0u, 0x31415926u}; + for (std::size_t round = 0; round < scalarValues.size(); ++round) { + const auto value = scalarValues[round]; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Input, value, ScalarCode.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Output[lane] == value + 1u, "scalar round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (a stale input)"); + } + std::printf("pipelined dispatch: scalar read round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains <= 1, "a dispatch drained the pipeline more than once (" + std::to_string(drains) + ")"); + } + LibcRunShutdown_nid_postfix(); + std::puts("pipelined dispatch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp new file mode 100644 index 0000000000..7e87976c54 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp @@ -0,0 +1,151 @@ +#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; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t low; + std::uint32_t high; + std::uint32_t expected[4]; +}; + +struct Case { + const char* name; + Shader shader; + std::span rows; +}; + +constexpr Vector PknormI16Rows[] = { + {0x0001u, 0x8001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x0155u, 0x8155u, {0x00000000u, 0xffff0001u, 0x00000000u, 0xffff0001u}}, + {0x0200u, 0x8200u, {0x00000000u, 0xffff0001u, 0x00000000u, 0xffff0001u}}, + {0x03ffu, 0x83ffu, {0x00000000u, 0xfffe0002u, 0x00000000u, 0xfffe0002u}}, + {0x8001u, 0x0001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8200u, 0x0200u, {0x00000000u, 0x0001ffffu, 0x00000000u, 0x0001ffffu}}, + {0x83ffu, 0x03ffu, {0x00000000u, 0x0002fffeu, 0x00000000u, 0x0002fffeu}}, + {0x0400u, 0x8400u, {0xfffe0002u, 0xfffe0002u, 0xfffe0002u, 0xfffe0002u}}, + {0x8400u, 0x0400u, {0x0002fffeu, 0x0002fffeu, 0x0002fffeu, 0x0002fffeu}}, + {0x0000u, 0x8000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8000u, 0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, 0xbc00u, {0x80017fffu, 0x80017fffu, 0x80017fffu, 0x80017fffu}}, + {0xbc00u, 0x3c00u, {0x7fff8001u, 0x7fff8001u, 0x7fff8001u, 0x7fff8001u}}, + {0x3800u, 0xb800u, {0xc0004000u, 0xc0004000u, 0xc0004000u, 0xc0004000u}}, + {0x7c00u, 0xfc00u, {0x80017fffu, 0x80017fffu, 0x80017fffu, 0x80017fffu}}, + {0x7e00u, 0xfe00u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, +}; + +constexpr Vector PknormU16Rows[] = { + {0x0001u, 0x8001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x0155u, 0x8155u, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u}}, + {0x0200u, 0x8200u, {0x00000000u, 0x00000002u, 0x00000000u, 0x00000002u}}, + {0x03ffu, 0x83ffu, {0x00000000u, 0x00000004u, 0x00000000u, 0x00000004u}}, + {0x8001u, 0x0001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8200u, 0x0200u, {0x00000000u, 0x00020000u, 0x00000000u, 0x00020000u}}, + {0x83ffu, 0x03ffu, {0x00000000u, 0x00040000u, 0x00000000u, 0x00040000u}}, + {0x0400u, 0x8400u, {0x00000004u, 0x00000004u, 0x00000004u, 0x00000004u}}, + {0x8400u, 0x0400u, {0x00040000u, 0x00040000u, 0x00040000u, 0x00040000u}}, + {0x0000u, 0x8000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8000u, 0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, 0xbc00u, {0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu}}, + {0xbc00u, 0x3c00u, {0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0000u}}, + {0x3800u, 0xb800u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x7c00u, 0xfc00u, {0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu}}, + {0x7e00u, 0xfe00u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, +}; + +const Case Cases[] = { + {"v_cvt_pknorm_i16_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd712000au, 0x00020b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u}}, PknormI16Rows}, + {"v_cvt_pknorm_u16_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd713000au, 0x00020b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u}}, PknormU16Rows}, +}; + +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, const Case& item, std::uint32_t denorm16, bool ieee) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].low; + Input[row * Inputs + 1] = item.rows[row].high; + } + 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(item.shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{denorm16 << 6u, true, ieee, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t denorm16, bool ieee) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto actual = Output[row]; + const auto expected = item.rows[row].expected[denorm16]; + Require(actual == expected, std::string(item.name) + " denorm16 " + std::to_string(denorm16) + (ieee ? " ieee" : " no ieee") + " input " + + Hex(item.rows[row].low) + " " + Hex(item.rows[row].high) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto& item : Cases) { + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + for (const bool ieee : {false, true}) { + Run(*device, item, denorm16, ieee); + Check(item, denorm16, ieee); + } + } + } + std::puts("pknorm f16 denormal 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/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 42ac67075a..b8276c8bb1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -1,7 +1,9 @@ +#include "../CommandBatchFailure.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "VulkanTestDevice.hpp" +#include "../AllocationFailure.hpp" #include #include #include @@ -12,6 +14,7 @@ #include #include #include +#include #include #include @@ -21,6 +24,7 @@ using AgcDriver::Graphics::BufferPool; using AgcDriver::Graphics::Check; using AgcDriver::Graphics::Context; using AgcDriver::Graphics::Require; +using AgcDriver::Tests::SetAllocationFailureCountdown; constexpr std::uint32_t Stale = 0xababababu; constexpr std::uint32_t Fresh = 0x11111111u; @@ -30,6 +34,76 @@ constexpr std::uint32_t PastEnd = 28; PFN_vkGetDeviceProcAddr RealDeviceProc = nullptr; PFN_vkCreateBuffer RealCreateBuffer = nullptr; +PFN_vkAllocateMemory RealAllocateMemory = nullptr; +PFN_vkFreeMemory RealFreeMemory = nullptr; +PFN_vkMapMemory RealMapMemory = nullptr; +PFN_vkUnmapMemory RealUnmapMemory = nullptr; +PFN_vkBindBufferMemory RealBindBufferMemory = nullptr; +VkResult BindFailure = VK_SUCCESS; +unsigned BindAttempts = 0; +unsigned SuccessfulBinds = 0; + +struct Allocation { + VkDeviceMemory memory = VK_NULL_HANDLE; + VkDeviceSize bytes = 0; + bool mapped = false; +}; + +std::array Allocations{}; +VkDeviceSize LiveBytes = 0; +unsigned AllocationErrors = 0; +int FailAfterAllocation = -1; +int FailAfterMap = -1; + +VKAPI_ATTR VkResult VKAPI_CALL CountedAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + const auto result = RealAllocateMemory(device, info, allocator, memory); + if (result == VK_SUCCESS) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [](const Allocation& allocation) { return allocation.memory == VK_NULL_HANDLE; }); + if (entry == Allocations.end()) ++AllocationErrors; + else *entry = {*memory, info->allocationSize, false}; + LiveBytes += info->allocationSize; + if (FailAfterAllocation >= 0) { + SetAllocationFailureCountdown(FailAfterAllocation); + FailAfterAllocation = -1; + } + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountedFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [&](const Allocation& allocation) { return allocation.memory == memory; }); + if (entry == Allocations.end() || memory == VK_NULL_HANDLE) { + ++AllocationErrors; + return; + } + LiveBytes -= entry->bytes; + *entry = {}; + RealFreeMemory(device, memory, allocator); +} + +VKAPI_ATTR VkResult VKAPI_CALL CountedMapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize bytes, VkMemoryMapFlags flags, void** mapping) { + const auto result = RealMapMemory(device, memory, offset, bytes, flags, mapping); + if (result == VK_SUCCESS) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [&](const Allocation& allocation) { return allocation.memory == memory; }); + if (entry == Allocations.end()) ++AllocationErrors; + else entry->mapped = true; + if (FailAfterMap >= 0) { + SetAllocationFailureCountdown(FailAfterMap); + FailAfterMap = -1; + } + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountedUnmapMemory(VkDevice device, VkDeviceMemory memory) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [&](const Allocation& allocation) { return allocation.memory == memory; }); + if (entry == Allocations.end() || !entry->mapped) { + ++AllocationErrors; + return; + } + entry->mapped = false; + RealUnmapMemory(device, memory); +} std::vector& CreatedSizes() { static std::vector sizes; @@ -41,8 +115,25 @@ VKAPI_ATTR VkResult VKAPI_CALL CountedCreateBuffer(VkDevice device, const VkBuff return RealCreateBuffer(device, info, allocator, buffer); } +VKAPI_ATTR VkResult VKAPI_CALL CountedBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + ++BindAttempts; + if (BindFailure != VK_SUCCESS) { + const auto result = BindFailure; + BindFailure = VK_SUCCESS; + return result; + } + const auto result = RealBindBufferMemory(device, buffer, memory, offset); + if (result == VK_SUCCESS) ++SuccessfulBinds; + return result; +} + VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(&CountedBindBufferMemory); if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(&CountedCreateBuffer); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(&CountedAllocateMemory); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(&CountedFreeMemory); + if (std::strcmp(name, "vkMapMemory") == 0) return reinterpret_cast(&CountedMapMemory); + if (std::strcmp(name, "vkUnmapMemory") == 0) return reinterpret_cast(&CountedUnmapMemory); return RealDeviceProc(device, name); } @@ -70,11 +161,13 @@ public: std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); const auto properties = function("vkGetPhysicalDeviceProperties"); + const auto* requested = std::getenv("ANYPS5_GPU"); + const bool named = requested != nullptr && *requested != '\0'; context.physical = devices.front(); for (const auto candidate : devices) { VkPhysicalDeviceProperties described{}; properties(candidate, &described); - if (described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + if (named ? std::strstr(described.deviceName, requested) != nullptr : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { context.physical = candidate; break; } @@ -82,7 +175,8 @@ public: VkPhysicalDeviceProperties selected{}; properties(context.physical, &selected); context.limits = selected.limits; - std::printf("device %s\n", selected.deviceName); + std::printf("device %s vendor=0x%x device=0x%x driver=0x%x\n", selected.deviceName, selected.vendorID, selected.deviceID, selected.driverVersion); + Require(requested == nullptr || std::strstr(selected.deviceName, requested) != nullptr, "selected device does not match ANYPS5_GPU"); VkPhysicalDeviceFeatures supported{}; function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); Require(supported.robustBufferAccess == VK_TRUE, "the device lacks robustBufferAccess"); @@ -108,6 +202,13 @@ public: RealDeviceProc = function("vkGetDeviceProcAddr"); RealCreateBuffer = reinterpret_cast(RealDeviceProc(context.device, "vkCreateBuffer")); Require(RealCreateBuffer != nullptr, "missing vkCreateBuffer"); + RealAllocateMemory = reinterpret_cast(RealDeviceProc(context.device, "vkAllocateMemory")); + RealFreeMemory = reinterpret_cast(RealDeviceProc(context.device, "vkFreeMemory")); + RealMapMemory = reinterpret_cast(RealDeviceProc(context.device, "vkMapMemory")); + RealUnmapMemory = reinterpret_cast(RealDeviceProc(context.device, "vkUnmapMemory")); + RealBindBufferMemory = reinterpret_cast(RealDeviceProc(context.device, "vkBindBufferMemory")); + Require(RealBindBufferMemory != nullptr, "missing vkBindBufferMemory"); + Require(RealAllocateMemory != nullptr && RealFreeMemory != nullptr && RealMapMemory != nullptr && RealUnmapMemory != nullptr, "missing Vulkan memory functions"); context.deviceProc = &CountingDeviceProc; function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -137,6 +238,13 @@ private: void release() noexcept { if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); context.bufferPool.reset(); + for (auto& allocation : Allocations) { + if (allocation.memory == VK_NULL_HANDLE) continue; + if (allocation.mapped) RealUnmapMemory(context.device, allocation.memory); + RealFreeMemory(context.device, allocation.memory, nullptr); + LiveBytes -= allocation.bytes; + allocation = {}; + } 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); @@ -148,6 +256,118 @@ private: Context context{}; }; +unsigned SlabMetadataFailure(const Context& context, std::uint32_t memoryType) { + const bool mapped = (context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0; + const auto initialBytes = LiveBytes; + unsigned rejected = 0; + unsigned failures = 0; + for (int point = 0; point < 16; ++point) { + const auto before = LiveBytes; + bool failed = false; + { + BufferPool pool(context); + AgcDriver::Graphics::SlabSlot slot{}; + (mapped ? FailAfterMap : FailAfterAllocation) = point; + try { + slot = pool.TakeSlot(context, memoryType, 512, false); + } catch (const std::bad_alloc&) { + failed = true; + ++rejected; + } + SetAllocationFailureCountdown(-1); + FailAfterMap = -1; + FailAfterAllocation = -1; + if (failed) { + const auto retained = LiveBytes - before; + std::printf("slab failure type=%u mapped=%u allocation=%d retained=%llu blocks=%zu\n", memoryType, mapped ? 1u : 0u, point, static_cast(retained), pool.SlabBlocks()); + if (retained != 0 || pool.SlabBlocks() != 0) ++failures; + slot = pool.TakeSlot(context, memoryType, 512, false); + } + Require((slot.mapping != nullptr) == mapped, "slab retry returned the wrong mapping state"); + pool.PutSlot(slot.memory, slot.offset); + } + if (LiveBytes != before) ++failures; + if (!failed) { + Require(rejected != 0, "native slab metadata failures were not exercised"); + std::printf("slab metadata type=%u failures=%u leaked=%llu\n", memoryType, rejected, static_cast(LiveBytes - initialBytes)); + return failures; + } + } + throw std::runtime_error("native slab failure injection never reached its success control"); +} + +void SlabMetadataFailures(const Context& context) { + unsigned failures = 0; + bool host = false; + bool device = false; + for (std::uint32_t type = 0; type < context.memory.memoryTypeCount; ++type) { + const auto flags = context.memory.memoryTypes[type].propertyFlags; + if (!host && (flags & HostMemory) == HostMemory) { + failures += SlabMetadataFailure(context, type); + host = true; + } else if (!device && (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) { + failures += SlabMetadataFailure(context, type); + device = true; + } + } + Require(host, "no coherent host-visible memory type for the slab failure test"); + std::printf("slab metadata total leaked bytes=%llu\n", static_cast(LiveBytes)); + Require(AllocationErrors == 0, "slab cleanup used an invalid memory handle or mapping"); + Require(failures == 0, "slab metadata allocation failure leaked native Vulkan memory or published an unusable block"); +} + +void DeviceBindingFailures(const Context& context) { + constexpr std::size_t bytes = std::size_t{3} << 20u; + constexpr std::size_t copiedBytes = 256; + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + unsigned failures = 0; + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + const auto initialBytes = LiveBytes; + { + auto probe = context; + probe.bufferPool = std::make_shared(probe); + AgcDriver::Graphics::Buffer upload(probe, copiedBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + AgcDriver::Graphics::Buffer readback(probe, copiedBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto before = LiveBytes; + const auto attempts = BindAttempts; + const auto bound = SuccessfulBinds; + BindFailure = result; + bool threw = false; + try { + AgcDriver::Graphics::DeviceBuffer failed(probe, bytes, usage); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("Vulkan result " + std::to_string(result)) != std::string::npos; + } + BindFailure = VK_SUCCESS; + const auto retained = LiveBytes - before; + const bool rejected = threw && BindAttempts == attempts + 1 && SuccessfulBinds == bound; + unsigned invalid = 0; + for (unsigned retry = 0; retry < 3; ++retry) { + AgcDriver::Graphics::DeviceBuffer recovered(probe, bytes, usage); + if (SuccessfulBinds != bound + 1) { + ++invalid; + continue; + } + std::memset(upload.Bytes().data(), 0x21 + retry, copiedBytes); + std::memset(readback.Bytes().data(), 0, copiedBytes); + AgcDriver::Graphics::CommandBatch batch(probe); + AgcDriver::Graphics::RecordMemoryBarrier(probe, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + AgcDriver::Graphics::CopyBuffer(probe, batch.Handle(), upload.Handle(), 0, recovered.Handle(), 0, copiedBytes); + AgcDriver::Graphics::RecordMemoryBarrier(probe, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + AgcDriver::Graphics::CopyBuffer(probe, batch.Handle(), recovered.Handle(), 0, readback.Handle(), 0, copiedBytes); + AgcDriver::Graphics::RecordMemoryBarrier(probe, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + readback.Invalidate(); + Require(std::memcmp(upload.Bytes().data(), readback.Bytes().data(), copiedBytes) == 0, "a recovered device buffer returned incorrect GPU copy data"); + } + if (!rejected || retained != 0 || invalid != 0 || BindAttempts != attempts + 2) ++failures; + std::printf("device bind result=%d retained=%llu invalid retries=%u binds=%u\n", static_cast(result), static_cast(retained), invalid, BindAttempts - attempts); + } + Require(LiveBytes == initialBytes && AllocationErrors == 0, "device bind failure cleanup leaked native Vulkan memory"); + } + Require(failures == 0, "a failed device buffer bind retained or reused an unbound buffer"); +} + void Emit(std::vector& words, std::uint32_t opcode, std::initializer_list operands) { words.push_back(static_cast((operands.size() + 1u) << 16u) | opcode); words.insert(words.end(), operands); @@ -332,8 +552,13 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + SlabMetadataFailures(device->GetContext()); RobustReadPastEnd(device->GetContext()); + DeviceBindingFailures(device->GetContext()); ExactSizes(device->GetContext()); + AgcDriver::Tests::RunCommandBatchFailureTests(device->GetContext(), true); + device.reset(); + Require(AllocationErrors == 0 && LiveBytes == 0, "native buffer cleanup left an invalid memory state"); std::puts("pooled buffer range tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp index 2233d80ba3..b67059d036 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -75,12 +76,23 @@ struct PixelHeader { class HeldPreparationThreads { public: HeldPreparationThreads() { - for (unsigned index = 0; index < std::thread::hardware_concurrency() + 2u; ++index) { + const unsigned hardwareConcurrency = std::thread::hardware_concurrency(); + const unsigned workerCount = std::max(2u, hardwareConcurrency / 2u); + for (unsigned index = 0; index < hardwareConcurrency + 2u; ++index) { AgcDriver::DriverDetail::SubmitRegistrationPreparation([state = state] { std::unique_lock lock(state->mutex); + ++state->active; + state->changed.notify_all(); state->changed.wait(lock, [&] { return state->released; }); }); } + std::unique_lock lock(state->mutex); + const bool workersBusy = state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return state->active >= workerCount; }); + lock.unlock(); + if (!workersBusy) { + Release(); + throw std::runtime_error("registration preparation workers did not become busy"); + } } ~HeldPreparationThreads() { @@ -99,6 +111,7 @@ private: struct State { std::mutex mutex; std::condition_variable changed; + unsigned active = 0; bool released = false; }; std::shared_ptr state = std::make_shared(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 777dd733f4..e242f0a0b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -4,7 +4,10 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" +#include "../NarrowConstantStoreFixture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include @@ -27,6 +30,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace { @@ -600,7 +609,7 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool header.specials.vgt_shader_stages_en.value = 0; ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "missing AGC user-data header"); header.shader.user_data = &header.userData; - if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr) { + if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr && std::getenv("APS5_LAZY_SHADER_PREPARE") == nullptr) { ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "v_readlane_b32 of lane 63 is outside"); } std::cout << "registered NGG preparation deferral passed; subgroup=" << target.subgroupSize << '\n'; @@ -612,6 +621,102 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool std::cout << "registered NGG wave64 preparation passed; subgroup=" << target.subgroupSize << '\n'; } +void LazyRegistration() { + using namespace NarrowConstantStoreFixture; + Require(std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr, "lazy preparation is not enabled"); + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + } header; + const auto address = reinterpret_cast(Code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(header); + header.shader.shader_size = sizeof(Code); + header.shader.code = Code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.specials = &header.specials; + header.specials.dispatch_modifier = 0x8000; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + ++header.registers[0].value; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "entry point is outside shader code"); + --header.registers[0].value; + header.registers[2].value = 0; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "must be nonzero"); + header.registers[2].value = Threads; + header.shader.num_sh_registers = 255; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata"); + header.shader.num_sh_registers = header.registers.size(); + const auto registered = [&] { + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "lazy registration published no shader registry"); + auto snapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &header.shader); + Require(snapshot != nullptr, "lazy registration published no snapshot"); + return snapshot; + }; + const auto lazySnapshot = registered(); + { + AgcDriver::DriverDetail::ShaderPreparationTransaction transaction; + const auto& state = transaction.Read(*lazySnapshot); + Require(state.deferred && state.entries.empty(), "lazy registration prepared the shader before its first dispatch"); + } + constexpr std::size_t outputBytes = 65536; +#ifdef _WIN32 + auto* allocation = static_cast(VirtualAlloc(nullptr, outputBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* allocation = static_cast(std::aligned_alloc(outputBytes, outputBytes)); +#endif + Require(allocation != nullptr, "cannot allocate lazy shader test output"); + const auto release = [](std::uint8_t* block) { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const std::unique_ptr ownedOutput(allocation, release); + const std::span output(allocation, outputBytes); + GuestAllocations::Mutation().Add(output.data(), output.size(), true, true, true); + const auto outputAddress = reinterpret_cast(output.data()); + std::vector commands; + for (const auto reg : header.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0027600u, 0x240, static_cast(outputAddress), static_cast(outputAddress >> 32u), 0xc0031500u, 1, 1, 1, 0x8041}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + for (unsigned iteration = 0; iteration < 2; ++iteration) { + std::ranges::fill(output, Fill); + sceAgcDriverSubmitAcb(0x20, &packet); + AgcDriverWaitIdle_nid_postfix(); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + for (std::size_t offset = 0; offset < output.size(); ++offset) { + const auto expected = offset < Threads * LaneBytes ? ExpectedLane[offset % LaneBytes] : Fill; + if (output[offset] != expected) throw std::runtime_error("lazy dispatch " + std::to_string(iteration) + " byte " + std::to_string(offset) + " is " + std::to_string(output[offset]) + ", expected " + std::to_string(expected)); + } + } + { + AgcDriver::DriverDetail::ShaderPreparationTransaction transaction; + Require(!transaction.Read(*lazySnapshot).entries.empty(), "the first dispatch did not prepare the lazily registered shader"); + } + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); + alignas(256) static const std::array invalid{0xbf9f0000u}; + const auto invalidAddress = reinterpret_cast(invalid.data()); + header.shader.code = invalid.data(); + header.shader.shader_size = sizeof(invalid); + header.registers[0].value = static_cast(invalidAddress >> 8u); + header.registers[1].value = static_cast(invalidAddress >> 40u); + AgcDriverRegisterShader_nid_postfix(&header.shader); + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); }, "s_code_end"); + commands[2] = header.registers[0].value; + commands[5] = header.registers[1].value; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "s_code_end"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "s_code_end"); +} + void DeferredRegistration() { alignas(256) std::array code{0xbe842104u, 0xbf810000u}; struct Header { @@ -672,7 +777,13 @@ void DeferredUndecodableRegistration() { int main(int argc, char** argv) { try { - Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup")), "invalid test arguments"); + Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); + if (argc == 2 && std::string_view(argv[1]) == "--lazy") { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + LazyRegistration(); + std::cout << "lazy shader preparation tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--registered-workgroup") { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp index 2654a113b7..79cdc00701 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp @@ -99,6 +99,46 @@ void Run(AgcDriver::VulkanDevice& device, VkPrimitiveTopology topology, std::spa } } +void RunRectangle(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& pixel) { + const auto rectangle = ShaderRecompiler::BuildRectListShaders(vertex, pixel, device.Target()); + Pixels.fill(Sentinel); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + 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_PATCH_LIST; + state.rectList = true; + state.cullMode = VK_CULL_MODE_NONE; + state.scissor = {{0, 0}, {Width, Height}}; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + const AgcDriver::Pm4::DrawParameters draw{0, 3, 0, 1, 0, false}; + for (std::uint32_t panel = 0; panel < 3; ++panel) { + const bool last = panel == 1; + state.viewport = {static_cast(panel * Panel), static_cast(Height), static_cast(Panel), -static_cast(Height), 0, 1}; + state.provokingVertexMode = last ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, + {ShaderRecompiler::ShaderStage::TessellationControl, last ? &rectangle.lastVertexControl : &rectangle.control, 0}, + {ShaderRecompiler::ShaderStage::TessellationEvaluation, &rectangle.evaluation, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &pixel, PixelPushOffset(0u, device.Target())} + }}; + device.Draw(state, draw, shaders); + } + device.WaitIdle(); + for (std::uint32_t panel = 0; panel < 3; ++panel) { + const auto& expected = panel == 1 ? Blue : Red; + const auto name = std::string("guest P0 rect list, ") + (panel == 1 ? "last" : "first") + " provoking vertex"; + for (const auto x : {16u, 48u}) { + for (const auto y : {16u, 48u}) CheckPixel(panel * Panel + x, y, expected, name); + } + CheckPixel(panel * Panel, 0, {64, 64, 64, 64}, name + " background"); + } +} + } int main() { @@ -133,7 +173,10 @@ int main() { {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, PixelPushOffset(0u, device->Target())} }}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, guestShaders, true, "guest P0"); - std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, first/last/first"); + auto exporting = vertex; + exporting.parameterExports = {0}; + RunRectangle(*device, exporting, guestPixel); + std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, guest P0 rect list, first/last/first"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp new file mode 100755 index 0000000000..00f3ca4d2e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); + +namespace { + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) constexpr std::array Code{0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("queue zero late interrupt: " + what); +} + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + check(block != nullptr, "cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, false); + return block; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_NO_OPPORTUNISTIC_REAP", "1"); +#else + setenv("APS5_NO_OPPORTUNISTIC_REAP", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(Code); + shader.code = Code.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + + auto* input = NewBlock(); + auto* output = NewBlock(); + auto* label = NewBlock(); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) input[lane] = 0x5b000000u | lane; + + KernelEqueue eq = 0; + check(sceKernelCreateEqueue(&eq, "AGC late interrupt") == 0, "event queue creation failed"); + auto owner = EqueuePin_nid_postfix(eq); + int tag = 0; + check(sceAgcDriverAddEqEvent(eq, 0, &tag) == 0, "graphics event registration failed"); + + const auto codeAddress = reinterpret_cast(Code.data()); + const auto labelAddress = reinterpret_cast(label); + const auto in = BufferDescriptor(input, BufferBytes); + const auto out = BufferDescriptor(output, BufferBytes); + std::vector dispatch{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, in[0], in[1], in[2], in[3], out[0], out[1], out[2], out[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + std::vector release{0xc0064900u, 0x06000528u, (2u << 29u) | (2u << 24u) | (1u << 16u), static_cast(labelAddress), static_cast(labelAddress >> 32u), 1u, 0u, 0u}; + const auto submit = [](std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + }; + const auto waitFor = [](const auto& done, const char* what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!done() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + check(done(), what); + }; + const auto dormant = [] { return AgcDriver::DriverDetail::queue0Dormant.load(std::memory_order_relaxed); }; + auto& pipeline = AgcDriver::DriverDetail::DrawPipeline::Queue0(); + + submit(dispatch); + waitFor([&] { return dormant() && pipeline.Outstanding() == 0; }, "queue 0 did not wait for work after the dispatch"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + submit(release); + waitFor([&] { return pipeline.Outstanding() != 0; }, "queue 0 did not hand the end-of-pipe label to the draw pipeline"); + waitFor(dormant, "queue 0 did not wait for work while the draw pipeline held the end-of-pipe label"); + } + + std::array events{}; + int delivered = 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((delivered = owner->GetTriggeredEvents(events.data(), 1)) == 0 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + check(delivered == 1 && events[0].udata == &tag, "an end-of-pipe interrupt the draw pipeline committed after queue 0 went idle was not delivered"); + + AgcDriverWaitIdle_nid_postfix(); + check(label[0] == 1u, "the end-of-pipe label was not written"); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) check(output[lane] == input[lane] + 1u, "the dispatch before the interrupt stored a wrong value"); + + check(sceAgcDriverDeleteEqEvent(eq, 0) == 0, "graphics event deletion failed"); + owner.reset(); + check(sceKernelDeleteEqueue(eq) == 0, "event queue deletion failed"); + AgcDriverShutdown_nid_postfix(); + std::puts("queue zero late interrupt 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/RectListIndirect.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp new file mode 100644 index 0000000000..560be5f9d5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp @@ -0,0 +1,234 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array SlotVertexCode{ + 0x7e000d05u, 0x100c00ffu, 0x3eaaaaabu, 0x7e0c4906u, 0x100e0cffu, 0x40400000u, 0x08000f00u, + 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e120d08u, 0x101212f4u, 0x060c0d09u, 0x100c0cf0u, 0x100202ffu, 0x3e000000u, 0x06020306u, 0x060202ffu, 0xbf600000u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array DirectMemory{}; +alignas(256) std::array IndirectMemory{}; +alignas(256) std::array EmptyMemory{}; +alignas(256) std::array DirectInstancedMemory{}; +alignas(256) std::array IndirectInstancedMemory{}; +alignas(256) std::array DirectIndexedMemory{}; +alignas(256) std::array IndirectIndexedMemory{}; +constexpr std::array RecordWords{3, 1, 0, 0, 0, 1, 0, 0, 6, 2, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0}; +constexpr std::size_t RecordBytes = 65536; +std::uint32_t* Records = nullptr; +alignas(256) std::array Indices{6, 7, 8, 3, 4, 5, 0, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header SlotVertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawRectangles() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto slotVertex = reinterpret_cast(SlotVertexCode.data()); + const auto indices = reinterpret_cast(Indices.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto records = reinterpret_cast(Records); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 17}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x11, {1, static_cast(records), static_cast(records >> 32u)}); + const auto target = [&](std::array& memory) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + }; + target(DirectMemory); + Append(commands, 0x2d, {3, 2}); + target(IndirectMemory); + Append(commands, 0x24, {0, 0, 0, 2}); + target(EmptyMemory); + Append(commands, 0x24, {16, 0, 0, 2}); + Append(commands, 0x76, {0xc8, static_cast(slotVertex >> 8u)}); + Append(commands, 0x76, {0xc9, static_cast(slotVertex >> 40u)}); + target(DirectInstancedMemory); + Append(commands, 0x2f, {2}); + Append(commands, 0x2d, {6, 2}); + Append(commands, 0x2f, {1}); + target(IndirectInstancedMemory); + Append(commands, 0x24, {32, 0, 0, 2}); + Append(commands, 0x2a, {0}); + target(DirectIndexedMemory); + Append(commands, 0x27, {6, static_cast(indices), static_cast(indices >> 32u), 6, 0}); + Append(commands, 0x26, {static_cast(indices), static_cast(indices >> 32u)}); + Append(commands, 0x13, {6}); + target(IndirectIndexedMemory); + Append(commands, 0x25, {48, 0, 0, 0}); + return commands; +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +std::string Slots(const std::array& memory) { + std::string slots; + for (std::size_t slot = 0; slot < 4; ++slot) { + std::size_t covered = 0; + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t x = slot * 16; x < slot * 16 + 16; ++x) { + const auto pixel = (row * 64 + x) * 4; + covered += std::any_of(memory.begin() + static_cast(pixel), memory.begin() + static_cast(pixel + 4), [](std::byte value) { return value != Kept; }) ? 1u : 0u; + } + } + if (!slots.empty()) slots += ' '; + slots += std::to_string(covered); + } + return slots; +} + +void Same(const std::array& indirect, const std::array& direct, const std::string& what) { + for (std::size_t index = 0; index < direct.size(); ++index) { + Require(indirect[index] == direct[index], "byte " + std::to_string(index) + " of the indirect " + what + " is " + std::to_string(static_cast(indirect[index])) + ", the direct draw wrote " + std::to_string(static_cast(direct[index]))); + } +} + +void DrawnOnTheGpu() { + const auto counts = AgcDriver::Graphics::IndirectDrawCounts(); + std::string cpu; + for (std::size_t path = 1; path < counts.size(); ++path) { + if (counts[path] == 0) continue; + if (!cpu.empty()) cpu += ", "; + cpu += std::to_string(counts[path]) + " " + AgcDriver::Graphics::IndirectDrawPathName(static_cast(path)); + } + Require(cpu.empty(), "indirect rect-list draws read their records on the CPU: " + cpu); + Require(counts[0] == 4, "the GPU drew " + std::to_string(counts[0]) + " indirect rect-list draws from their records, expected 4"); +} + +void IndirectRectListsDrawAsDirectOnes() { + DirectMemory.fill(Kept); + IndirectMemory.fill(Kept); + EmptyMemory.fill(Kept); + DirectInstancedMemory.fill(Kept); + IndirectInstancedMemory.fill(Kept); + DirectIndexedMemory.fill(Kept); + IndirectIndexedMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(SlotVertexHeader, SlotVertexCode.data(), sizeof(SlotVertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + Records = static_cast(::operator new(RecordBytes, std::align_val_t{RecordBytes})); + std::copy(RecordWords.begin(), RecordWords.end(), Records); + { + GuestAllocations::Mutation mutation; + mutation.Add(Records, RecordBytes, true, true, true); + } + auto commands = DrawRectangles(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto direct = Covered(DirectMemory); + Require(direct != 0, "the direct rect-list draw covered nothing"); + Same(IndirectMemory, DirectMemory, "rect-list draw"); + const auto empty = Covered(EmptyMemory); + Require(empty == 0, "the indirect rect-list draw whose record has no vertices covered " + std::to_string(empty) + " bytes, expected none"); + Require(Slots(DirectInstancedMemory) == "32 32 32 32", "the direct rect-list draw of 6 vertices and 2 instances covered pixels " + Slots(DirectInstancedMemory) + " of its four rectangles, expected 32 each"); + Same(IndirectInstancedMemory, DirectInstancedMemory, "rect-list draw of 6 vertices and 2 instances"); + Require(Slots(DirectIndexedMemory) == "0 32 32 0", "the direct indexed rect-list draw covered pixels " + Slots(DirectIndexedMemory) + " of the four rectangles, expected only the second and third"); + Same(IndirectIndexedMemory, DirectIndexedMemory, "indexed rect-list draw"); + DrawnOnTheGpu(); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + IndirectRectListsDrawAsDirectOnes(); + AgcDriverShutdown_nid_postfix(); + std::puts("indirect rect-list draws were drawn on the GPU and drew what the direct draw did"); + 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/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index f3d7e2bf44..dfe6cfab4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" @@ -25,9 +26,20 @@ #include #include #include +#include #include #include +#include #include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} namespace { @@ -54,6 +66,8 @@ constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::uint32_t FillOnes = 0x3c003c00u; constexpr std::array Ones{1.0f, 1.0f, 1.0f, 1.0f}; +constexpr std::array Written{0.75f, 0.25f, 0.5f, 1.0f}; +constexpr std::uint32_t StoredSample = 1; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -70,6 +84,18 @@ alignas(256) constexpr std::array LoadCode{ 0x80001301, 0xbf810000, }; +alignas(256) constexpr std::array StoreCode{ + 0x34020087, 0xe030100c, 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xe0301018, + 0x80001001, 0xe030101c, 0x80001101, 0xe0301020, 0x80001201, 0xe0301024, 0x80001301, 0xbf8c3f70, + 0xf0201f30, 0x00011005, 0xbf810000, +}; + +struct SampleWrite { + std::uint32_t x; + std::uint32_t y; + std::uint32_t sample; +}; + constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} }}; @@ -142,7 +168,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c return state; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t indexAddress = 0, std::uint32_t indexSize = 2u) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -181,7 +207,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} }}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + const AgcDriver::Pm4::DrawParameters draw{indexAddress, static_cast(Triangle.size()), indexAddress != 0 ? indexSize : 0u, 1, 0, indexAddress != 0}; device.Draw(state, draw, shaders); } @@ -199,13 +225,12 @@ std::array TargetDescriptor(const Block& block, std::uint32_t }; } -ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData) { +ShaderRecompiler::RecompileResult CompileProgram(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, std::vector& userData) { const auto address = reinterpret_cast(Buffer.data()); const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; userData.assign(16, 0u); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); - const std::span code(LoadCode); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -218,6 +243,63 @@ ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, c return ShaderRecompiler::Recompile(request); } +ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData) { + return CompileProgram(device, LoadCode, texture, userData); +} + +void StoreSamples(AgcDriver::VulkanDevice& device, const std::array& texture, const std::vector& writes) { + std::vector userData; + const auto program = CompileProgram(device, StoreCode, texture, userData); + for (std::size_t first = 0; first < writes.size(); first += Threads) { + Buffer.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto& write = writes[std::min(first + tid, writes.size() - 1)]; + const std::array words{0u, 0u, 0u, write.x, write.y, write.sample, std::bit_cast(Written[0]), std::bit_cast(Written[1]), std::bit_cast(Written[2]), std::bit_cast(Written[3])}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); + device.WaitIdle(); + } +} + +std::vector SampleWrites(bool (*selected)(std::uint32_t x, std::uint32_t y, std::uint32_t sample)) { + std::vector writes; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + if (selected(x, y, sample)) writes.push_back({x, y, sample}); + } + } + } + return writes; +} + +void CheckWritten(const std::vector>& before, const std::vector>& after, bool (*selected)(std::uint32_t x, std::uint32_t y, std::uint32_t sample), const std::string& what) { + std::uint32_t written = 0; + for (std::uint32_t index = 0; index < after.size(); ++index) { + const auto pixel = index / Samples; + const auto x = pixel % Side; + const auto y = pixel / Side; + const auto sample = index % Samples; + const bool chosen = selected(x, y, sample); + char where[96]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u)", sample, x, y); + Require(after[index] == (chosen ? Written : before[index]), std::string(where) + (chosen ? " does not hold the stored color " : " changed although it was not stored to ") + what); + written += chosen ? 1u : 0u; + } + Require(written != 0 && written != after.size(), "the stores covered every sample or none " + what); +} + +void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + StoreSamples(device, texture, {{0u, 0u, 0u}}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { std::vector userData; const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift), userData); @@ -293,6 +375,69 @@ void CheckUniform(const std::vector>& loaded, const std::ar } } +void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, const Block& destination) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (context.hostImportAlignment == 0) { + std::puts("native index alias checks not run: host imports unavailable"); + return; + } + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(DecodeTarget(source)); + Require(colorBytes <= source.bytes && source.bytes - colorBytes >= BlockBytes, "the native-index fixture has no neighboring block"); + const auto neighbor = source.Address() + colorBytes; + AgcDriver::Graphics::RequireNoMultisampleTarget(neighbor, 12u, "an index neighbor control"); + std::string guarded; + try { + AgcDriver::Graphics::RequireNoMultisampleTarget(source.Address(), 48u, "a draw-input control"); + } catch (const std::exception& error) { + guarded = error.what(); + } + Require(guarded.find("whose samples are only on the host") != std::string::npos, "the draw-input source is not guarded as a native target"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* native = AgcDriver::Graphics::HostImportFor(context, source.Address(), 12u); + const auto* adjacent = AgcDriver::Graphics::HostImportFor(context, neighbor, 12u); + Require(native != nullptr && native == adjacent, "the native indices and their neighbor do not share a host import"); + } + const auto state = TargetState(DecodeTarget(destination)); + AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast(colorBytes), FillOnes); + std::uint32_t missed = 0; + for (const std::uint32_t indexSize : {2u, 4u}) { + if (indexSize == 2u) { + const std::array indices{0u, 1u, 2u}; + std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices)); + } else { + const std::array indices{0u, 1u, 2u}; + std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices)); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::NoteColorMetadataFill(destination.Address(), static_cast(AgcDriver::Graphics::MultisampledColorBytes(state.color)), 0u); + AgcDriver::Graphics::MaterializeMultisampleTargetClear(context, state.color); + } + CheckUniform(LoadSamples(device, destination), {}, "before the neighboring indexed draw"); + Draw(device, state, neighbor, indexSize); + device.WaitIdle(); + CheckSamples(state, LoadSamples(device, destination), {}); + for (const bool edge : {false, true}) { + const auto address = edge ? neighbor - indexSize : source.Address(); + std::string refusal; + try { + Draw(device, state, address, indexSize); + device.WaitIdle(); + } catch (const std::exception& error) { + refusal = error.what(); + } + const bool refused = refusal.find("whose samples are only on the host") != std::string::npos; + std::printf("native index %u %s: refused=%u neighbor=passed detail=%s\n", indexSize * 8u, edge ? "edge" : "base", refused ? 1u : 0u, refusal.c_str()); + Require(refusal.empty() || refused, "the native index input failed for another reason: " + refusal); + missed += refused ? 0u : 1u; + } + } + CheckUniform(LoadSamples(device, source), Ones, "after native draw-input alias attempts"); + std::puts("native index controls: shared-import=1 source-samples=1024 neighbor-triangle=passed"); + Require(missed == 0, "native indices missed " + std::to_string(missed) + " resident-image refusals"); +} + void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) { for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word)); } @@ -311,16 +456,137 @@ void RequireRefused(AgcDriver::VulkanDevice& device, const std::array VertexValues{}; + +void DrawVertexInput(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t address, std::uint32_t stride) { + alignas(256) static constexpr std::array code{ + 0xbe822100u, 0x34020a84u, 0xe0781000u, 0x80010801u, 0xf80008cfu, 0x0b0a0908u, 0xbf810000u, + }; + VertexValues.fill(0xdeadbeefu); + const auto output = reinterpret_cast(VertexValues.data()); + std::vector userData(12u, 0u); + userData[0] = 0x10000000u; + userData[1] = 1u; + userData[4] = static_cast(output); + userData[5] = static_cast((output >> 32u) & 0xffffu); + userData[6] = static_cast(sizeof(VertexValues)); + userData[7] = 0x31016facu; + ShaderRecompiler::ShaderVertexStageInfo info{}; + info.resourcesNum = 1u; + info.resources[0].fields = {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), stride == 0u ? 16u : 3u, 0x3104dfacu}; + info.resourcesDst[0] = {8, 4, 0, 0}; + info.fetchAttribReg = 8u; + info.fetchBufferReg = 10u; + info.fetchEmbedded = true; + const std::array vertexMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {64u, 0, userData, std::nullopt, std::nullopt, info, vertexMemory}, + device.Target(), {0, 0, 0, 64} + }; + vertexRequest.useCache = false; + const auto vertex = ShaderRecompiler::Recompile(vertexRequest); + Require(vertex.vertexAttributes.size() == 1u, "the native vertex probe did not create a fixed-function attribute"); + const auto vertexPush = static_cast(vertex.pushConstants.size()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + const std::array pixelMemory{{{pixelAddress, std::as_bytes(std::span(PixelCode))}}}; + const AgcDriver::Registers registers{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + ShaderRecompiler::RecompileRequest pixelRequest{ + {ShaderStage::Fragment, pixelAddress, PixelCode, 0, {}}, + {64u, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(registers, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + device.Target(), PixelPushLayout(vertexPush, device.Target()) + }; + pixelRequest.useCache = false; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, device.Target())}}}; + const AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); } -int main() { +void CheckVertexValues(std::uint64_t address, std::uint32_t stride) { + std::array expected{}; + for (std::size_t vertex = 0; vertex < 3u; ++vertex) { + std::memcpy(expected.data(), reinterpret_cast(address + vertex * stride), sizeof(expected)); + for (std::size_t component = 0; component < expected.size(); ++component) { + Require(VertexValues[vertex * 4u + component] == expected[component], "the fixed-function vertex probe did not observe the original guest bytes"); + } + } +} + +int NativeVertexInput(std::unique_ptr& device) { + Block source(4u * BlockBytes); + Block destination(BlockBytes); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (context.hostImportAlignment == 0u) { + std::puts("native vertex input not tested: host imports unavailable"); + return VulkanTestSkipped; + } + const auto color = DecodeTarget(source); + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes >= 48u && colorBytes + 48u <= source.bytes, "the native vertex probe has no neighboring range"); + constexpr std::array element{0.25f, -2.0f, 7.5f, 1.0f}; + for (std::size_t vertex = 0; vertex < 3u; ++vertex) { + std::memcpy(source.data + vertex * sizeof(element), element.data(), sizeof(element)); + std::memcpy(source.data + colorBytes + vertex * sizeof(element), element.data(), sizeof(element)); + } + std::memcpy(source.data + colorBytes - 8u, element.data(), 8u); + Draw(*device, TargetState(color)); + AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast(colorBytes), FillOnes); + CheckUniform(LoadSamples(*device, source), Ones, "before the fixed-function vertex probe"); + const auto neighbor = source.Address() + colorBytes; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* native = AgcDriver::Graphics::HostImportFor(context, source.Address(), 48u); + const auto* adjacent = AgcDriver::Graphics::HostImportFor(context, neighbor, 48u); + Require(native != nullptr && native == adjacent, "the native and neighboring vertices do not share an import"); + } + AgcDriver::Graphics::RequireNoMultisampleTarget(neighbor, 48u, "the vertex neighbor control"); + const auto state = TargetState(DecodeTarget(destination)); + std::uint32_t missed = 0u; + for (const auto stride : {0u, 16u}) { + for (const bool edge : {false, true}) { + DrawVertexInput(*device, state, neighbor, stride); + CheckVertexValues(neighbor, stride); + const auto address = edge ? neighbor - 8u : source.Address(); + std::string refusal; + try { + DrawVertexInput(*device, state, address, stride); + CheckVertexValues(address, stride); + } catch (const std::exception& error) { + refusal = error.what(); + } + const bool refused = refusal.find("whose samples are only on the host") != std::string::npos; + Require(refusal.empty() || refused, "the native vertex input failed for another reason: " + refusal); + std::printf("native vertex stride %u %s: refused=%u neighbor=passed original-guest-values=%u detail=%s\n", stride, edge ? "edge" : "base", refused ? 1u : 0u, refused ? 0u : 12u, refusal.c_str()); + missed += refused ? 0u : 1u; + } + } + CheckUniform(LoadSamples(*device, source), Ones, "after fixed-function vertex inputs"); + std::puts("native vertex controls: shared-import=1 source-samples=1024 neighbor-values=passed"); + Require(missed == 0u, "native vertex inputs missed " + std::to_string(missed) + " resident-image refusals"); + return 0; +} + +} + +int main(int argc, char** argv) { try { - const auto device = OpenVulkanTestDevice(); + auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device); Block block(4u * BlockBytes); Block other(BlockBytes); const auto color = DecodeTarget(block); const auto state = TargetState(color); + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { + std::puts("multisampled target sampling not tested: storage image multisampling unavailable"); + return VulkanTestSkipped; + } Draw(*device, state); CheckSamples(state, LoadSamples(*device, block), {}); const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); @@ -341,7 +607,26 @@ int main() { const std::uint32_t odd = 0x3c000000u; std::memcpy(mixed.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); RequireRefused(*device, TargetDescriptor(mixed, SampleShift), "does not hold one repeated 32-bit value", "loading a multisampled texture no draw rendered to whose color memory is not uniform"); + const auto oneSample = [](std::uint32_t, std::uint32_t, std::uint32_t sample) { return sample == StoredSample; }; + const auto drawnBefore = LoadSamples(*device, block); + StoreSamples(*device, TargetDescriptor(block, SampleShift), SampleWrites(oneSample)); + CheckWritten(drawnBefore, LoadSamples(*device, block), oneSample, "into one sample of each pixel of the drawn target"); + const auto leftHalf = [](std::uint32_t x, std::uint32_t, std::uint32_t sample) { return x < Side / 2u && sample != 0u; }; + Block undrawn(BlockBytes); + StoreSamples(*device, TargetDescriptor(undrawn, SampleShift), SampleWrites(leftHalf)); + CheckWritten(std::vector>(Side * Side * Samples), LoadSamples(*device, undrawn), leftHalf, "into zeroed color memory no draw rendered to"); + Block mixedStore(BlockBytes); + StoreWords(mixedStore, colorBytes, FillOnes); + std::memcpy(mixedStore.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); + RequireStoreRefused(*device, TargetDescriptor(mixedStore, SampleShift), "writing the 16x16 multisampled texture", "storing to a multisampled texture no draw rendered to whose color memory is not uniform"); + std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed color memory no draw rendered to reach exactly those samples, and a store to non-uniform color memory no draw rendered to is refused"); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); + Block destination(BlockBytes); + struct ReleaseIndexDevice { + std::unique_ptr& device; + ~ReleaseIndexDevice() { device.reset(); } + } releaseIndexDevice{device}; + CheckNativeIndices(*device, block, destination); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 9d1978dbb2..a1b31188b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" @@ -236,6 +237,44 @@ std::shared_ptr ImageResources(const Context& context, const Bl return std::make_shared(context, shader); } +void recorderEvictionTests(const Context& context, const Block& block) { + auto& cache = SharedResourceCache(); + cache.Clear(); + ClearCachedTextures(context.device); + auto sampled = ImageResources(context, block, 0, 1, true); + const auto images = sampled->StorageImages(); + Require(images.size() == 1, "the eviction fixture has no sampled storage image"); + const std::weak_ptr resources = sampled; + const std::weak_ptr view = sampled->SampledTextures().front(); + const std::weak_ptr image = sampled->SampledTextures().front()->SharedStorageSource(); + cache.Insert({1}, sampled); + Require(cache.Find({1}) == sampled, "the recorder fixture's descriptor set was not cached"); + sampled.reset(); + Buffer readback(context, sizeof(std::uint32_t), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + Recorder recorder(context); + recorder.Activate(); + recorder.Commands(); + for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(cache.Find({1}) == nullptr, "storage eviction left the recorded descriptor set cached"); + Require(!resources.expired() && !view.expired() && !image.expired(), "eviction destroyed the recorder's descriptor set, view or image"); + Require(!image.lock()->Cached(), "the recorder fixture did not evict the source image"); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, 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")(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 pixel = 0; + std::memcpy(&pixel, readback.Bytes().data(), sizeof(pixel)); + Require(pixel == 0x5a5a5a5a, "eviction changed the recorder's storage image pixel"); + } + Require(resources.expired() && view.expired() && image.expired(), "an evicted image outlived its completed recorder"); + std::puts("storage eviction recorder lifetime and pixel readback passed"); +} + bool resourceCacheTests(Context context, const std::string& mode) { PlaceholderImages placeholders(context); context.placeholderImages = &placeholders; @@ -281,7 +320,6 @@ bool resourceCacheTests(Context context, const std::string& mode) { Require(cache.Size() == 0 && displaced.empty(), sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache"); image.reset(); oversized.reset(); - TextureCacheUsage(); Require(rejected.expired() && early.expired(), "the rejected oversized descriptor set retained an evicted storage image"); std::puts(sampledImages ? "resource cache oversized sampled lifetime passed" : "resource cache oversized storage lifetime passed"); ClearCachedTextures(context.device); @@ -313,10 +351,10 @@ bool resourceCacheTests(Context context, const std::string& mode) { Require(storageSet.expired() && !sampledSet.expired() && !staleImage.expired(), "stale storage release did not preserve the sampled caller's image"); std::puts("resource cache stale storage caller release passed"); sampled.reset(); - TextureCacheUsage(); Require(sampledSet.expired() && staleImage.expired(), "the rejected descriptor sets retained an evicted storage image"); std::puts("resource cache stale sampled caller release passed"); } + if (mode == "resource-admission") recorderEvictionTests(context, block); } catch (...) { cache.Clear(); throw; @@ -339,7 +377,7 @@ void viewTests(const Context& context) { auto view = Sampled(context, block.Surface(0)); Require(view->ViewsStorageImage(), "a sampled texture in host-imported memory is not a view of its storage image"); const std::weak_ptr viewed = view; - const auto* image = view->StorageSource(); + const std::weak_ptr storage = view->SharedStorageSource(); const auto after = TextureCacheUsage(); Require(after.sampledEntries == before.sampledEntries + 1u, "the view did not enter the sampled texture cache"); Require(after.sampledBytes == before.sampledBytes, "a view of a cached storage image counted " + std::to_string(after.sampledBytes - before.sampledBytes) + " bytes against the sampled texture cache"); @@ -347,12 +385,18 @@ void viewTests(const Context& context) { Require(Sampled(context, block.Surface(0)) == view, "the second lookup of the surface did not hit its view"); view.reset(); for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); - Require(!StorageImageCached(context, image), "the storage image cache never evicted the viewed image under a 1 MiB budget"); + Require(viewed.expired() && storage.expired(), "the sampled texture cache kept the view of an evicted storage image"); const auto evicted = TextureCacheUsage(); - Require(viewed.expired(), "the sampled texture cache kept the view of an evicted storage image"); Require(evicted.sampledEntries == after.sampledEntries - 1u && evicted.sampledBytes == before.sampledBytes, "dropping the view of the evicted storage image changed other sampled entries"); auto again = Sampled(context, block.Surface(0)); Require(again->ViewsStorageImage() && again->StorageSource() != nullptr && StorageImageCached(context, again->StorageSource()), "the surface's next lookup does not view its new cached storage image"); + const std::weak_ptr heldView = again; + const std::weak_ptr heldStorage = again->SharedStorageSource(); + for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(!StorageImageCached(context, again->StorageSource()), "the held view's source was not evicted"); + Require(!heldView.expired() && !heldStorage.expired(), "eviction destroyed an active caller's view or image"); + again.reset(); + Require(heldView.expired() && heldStorage.expired(), "the evicted image outlived its last caller without a sampled lookup"); } void reportedBudgetTests(const Context& context) { @@ -580,6 +624,21 @@ std::uint64_t ReadBudget(const Context& context) { return SampledTextureCacheBudget(context); } +constexpr std::uint64_t PressedUsage = ReportedBudget - 11ull * (1ull << 30u); + +void VKAPI_PTR ReportPressure(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + for (std::uint32_t heap = 0; heap < VK_MAX_MEMORY_HEAPS; ++heap) { + reported->heapBudget[heap] = ReportedBudget; + reported->heapUsage[heap] = PressedUsage; + } +} + +std::uint64_t ReadStorageBudget(const Context& context) { + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + return StorageTextureCacheBudget(context); +} + void memoryTests(Context context) { context.memoryProperties2 = &ReportBudget; const auto before = SampledTextureMemory(); @@ -597,6 +656,20 @@ void memoryTests(Context context) { Require(SampledTextureMemory() == before, "a released texture still counts as sampled texture memory"); } +void storageMemoryTests(Context context) { + constexpr std::uint64_t GiB = 1ull << 30u; + context.memoryProperties2 = &ReportBudget; + Block block(context); + auto held = Sampled(context, block.Surface(0)); + const auto quarter = std::min(StorageTextureBudget(context.memory, nullptr, 0, 0), ReportedBudget / 4u); + Require(ReadStorageBudget(context) == quarter, "52 GiB free of a 64 GiB budget does not leave the storage image cache its quarter"); + context.memoryProperties2 = &ReportPressure; + const auto pressed = ReadStorageBudget(context); + const auto expected = GiB + SampledTextureMemory() + TextureCacheUsage().storageBytes; + Require(pressed == expected, "53 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures' 2 GiB floor and 8 GiB of headroom"); + std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast(ReportedBudget >> 20u), static_cast(PressedUsage >> 20u), static_cast(pressed >> 20u)); +} + } int main(int argc, char** argv) { @@ -629,6 +702,7 @@ int main(int argc, char** argv) { if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); + storageMemoryTests(context); ClearCachedTextures(context.device); std::puts("sampled texture memory budget tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp new file mode 100644 index 0000000000..8f835d52de --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include "CompiledVariant.hpp" +#include "ShaderDiskCache.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +void ClosingStartsNoPreparation() { + using namespace AgcDriver::DriverDetail; + Require(RegistrationPreparationThreads() == 0, "a shader preparation thread ran before anything was prepared"); + CloseRegistrationPreparation(); + Require(RegistrationPreparationThreads() == 0, "closing the shader preparation started preparation threads"); + const auto caller = std::this_thread::get_id(); + bool ranHere = false; + SubmitRegistrationPreparation([&] { ranHere = std::this_thread::get_id() == caller; }); + SubmitLibraryPrebuild([&] { ranHere = ranHere && std::this_thread::get_id() == caller; }); + Require(ranHere, "a preparation submitted after closing ran on another thread"); + Require(RegistrationPreparationThreads() == 0, "a preparation submitted after closing started preparation threads"); +} + +void ClosingJoinsPreparation() { + using namespace AgcDriver::DriverDetail; + std::promise prepared; + std::promise prebuilt; + auto preparedOn = prepared.get_future(); + auto prebuiltOn = prebuilt.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + SubmitRegistrationPreparation([&] { prepared.set_value(std::this_thread::get_id()); opened.wait(); }); + SubmitLibraryPrebuild([&] { prebuilt.set_value(std::this_thread::get_id()); opened.wait(); }); + const auto caller = std::this_thread::get_id(); + const bool offCaller = preparedOn.get() != caller && prebuiltOn.get() != caller; + auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); + const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + gate.set_value(); + closing.get(); + Require(offCaller, "a queued preparation ran on the caller instead of a preparation thread"); + Require(waited, "closing returned while preparation tasks were still running"); + Require(RegistrationPreparationThreads() == 0, "closing the shader preparation left its threads running"); + CloseRegistrationPreparation(); + bool ranHere = false; + SubmitRegistrationPreparation([&] { ranHere = std::this_thread::get_id() == caller; }); + SubmitLibraryPrebuild([&] { ranHere = ranHere && std::this_thread::get_id() == caller; }); + Require(ranHere && RegistrationPreparationThreads() == 0, "a closed preparation pool restarted after being joined"); +} + +void StoppingJoinsCompareHelpers() { + using namespace AgcDriver::Graphics; + const std::vector first(std::size_t{2} << 20u, std::byte{1}); + auto second = first; + second.back() = std::byte{2}; + const auto half = first.size() / 2u; + const std::array spans{{{first.data(), second.data(), half}, {first.data() + half, second.data() + half, half}}}; + std::array equal{}; + CompareSpans(spans, equal); + Require(equal[0] == 1u && equal[1] == 0u, "the compare helpers compared the spans wrongly"); + Require(CompareHelpersRunning() == CompareHelpers(), "a parallel compare started no compare helpers"); + StopCompareHelpers(); + Require(CompareHelpersRunning() == 0, "stopping the compare helpers left them running"); + equal = {}; + CompareSpans(spans, equal); + Require(equal[0] == 1u && equal[1] == 0u, "a compare after the helpers stopped compared the spans wrongly"); + Require(CompareHelpersRunning() == 0, "a compare after the helpers stopped started them again"); +} + +void ShutdownJoinsDrawCommitter() { + using namespace AgcDriver::DriverDetail; + auto& pipeline = DrawPipeline::Queue0(); + const auto caller = std::this_thread::get_id(); + std::promise started; + auto startedOn = started.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + bool queuedRan = false; + pipeline.Enqueue([&started, opened] { + started.set_value(std::this_thread::get_id()); + opened.wait(); + }, {}); + pipeline.Enqueue([&] { queuedRan = true; }, {}); + Require(startedOn.get() != caller, "a pipelined commit ran on the caller instead of the draw committer"); + Require(DrawPipeline::Committers() == 1, "a pipelined commit started no draw committer"); + gate.set_value(); + AgcDriverShutdown_nid_postfix(); + Require(DrawPipeline::Committers() == 0, "the driver's shutdown left the draw committer running"); + Require(queuedRan, "the driver's shutdown dropped a pipelined commit"); + bool ranHere = false; + pipeline.Enqueue([&] { ranHere = std::this_thread::get_id() == caller; }, {}); + Require(ranHere, "a commit enqueued after the shutdown ran on another thread"); + Require(DrawPipeline::Committers() == 0, "a commit enqueued after the shutdown started a draw committer"); +} + +void ShutdownJoinsShaderCacheWriter(bool lateInitialization = false) { + namespace Cache = ShaderRecompiler::ShaderDiskCache; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shutdown-shader-cache-" + std::to_string(std::random_device{}())); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + if (lateInitialization) AgcDriverShutdown_nid_postfix(); + Require(Cache::Enabled(), "the shader cache is not enabled"); + Require(Cache::Writers() == (lateInitialization ? 0u : 1u), "the shader cache writer did not respect the shutdown state"); + const auto variant = std::make_shared(); + const std::vector queued{std::byte{1}}; + const std::vector late{std::byte{2}}; + Cache::Store(queued, variant); + AgcDriverShutdown_nid_postfix(); + Require(Cache::Writers() == 0, "the driver's shutdown left the shader cache writer running"); + Require(std::filesystem::exists(Cache::EntryDirectory() / Cache::EntryName(queued)), "the driver's shutdown dropped a queued shader cache write"); + Cache::Store(late, variant); + Require(std::filesystem::exists(Cache::EntryDirectory() / Cache::EntryName(late)), "a shader cache store after the shutdown was not written at once"); + Require(Cache::Writers() == 0, "a shader cache store after the shutdown started a writer"); + std::error_code error; + std::filesystem::remove_all(directory, error); +} + +void ShutdownPassesHeldAsyncLog() { + static_cast(AgcDriver::DriverDetail::Driver::Get()); + std::promise held; + std::promise release; + auto released = release.get_future(); + std::thread holder([&held, &released] { + const auto lock = AgcDriver::HoldAsyncLog(); + held.set_value(); + released.wait(); + }); + held.get_future().wait(); + auto shutdown = std::async(std::launch::async, [] { AgcDriverShutdown_nid_postfix(); }); + const bool returned = shutdown.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + release.set_value(); + holder.join(); + shutdown.get(); + Require(returned, "the driver's shutdown waited for the async log's lock"); +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? argv[1] : ""; + if (mode == "--joined") ClosingJoinsPreparation(); + else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); + else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); + else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); + else if (mode == "--late-shader-cache") ShutdownJoinsShaderCacheWriter(true); + else if (mode == "--async-log-held") ShutdownPassesHeldAsyncLog(); + else if (mode == "--exit-after-start") static_cast(AgcDriver::DriverDetail::Driver::Get()); + else ClosingStartsNoPreparation(); + std::cout << "shutdown threads 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/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 6c271a9f48..680b8fcfa8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -24,11 +24,20 @@ int main() { return VulkanTestSkipped; } #else +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; +constexpr std::uint64_t LargestTestedLimit = std::uint64_t{1} << 30u; std::uint8_t* mapping = nullptr; std::uint64_t mappingBytes = 0; @@ -56,6 +65,12 @@ bool Imported(VkDevice device, std::uint64_t address, std::size_t bytes) { return AgcDriver::Graphics::HostImportCovers(probe, address, bytes); } +bool DmaBufImport(const AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = AgcDriver::Graphics::HostImportFor(AgcDriver::VulkanDeviceTestAccess::ContextOf(device), address, bytes); + return import != nullptr && import->dmaBuf; +} + } int main() { @@ -66,6 +81,10 @@ int main() { return VulkanTestSkipped; } close(udmabuf); + if (UdmabufLimitBytes() > LargestTestedLimit) { + std::printf("skipped, udmabuf size_limit_mb is above %llu: a range above it is too large to map\n", static_cast(LargestTestedLimit >> 20u)); + return VulkanTestSkipped; + } auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -107,21 +126,30 @@ int main() { auto* small = mapping + bytes; constexpr std::uint64_t SmallBytes = std::uint64_t{1} << 20u; + const bool settles = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).settleQueue != VK_NULL_HANDLE; + const auto settledBefore = AgcDriver::Graphics::HostImportsSettled(); device->ImportGuestMemory(Register(small, SmallBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + const auto settledAtMap = AgcDriver::Graphics::HostImportsSettled(); const bool smallImported = Imported(device->Device(), reinterpret_cast(small), static_cast(SmallBytes)); + const bool settlesShared = settles && smallImported && DmaBufImport(*device, reinterpret_cast(small), static_cast(SmallBytes)); GuestAllocations::Mutation().Remove(small); if (!smallImported) { std::printf("skipped, the device does not import dma-buf memory\n"); return VulkanTestSkipped; } + if (!settles) std::printf("note: the device has no settle queue, so dma-buf imports are not settled\n"); + else if (!settlesShared) std::printf("note: the device imports shared memory through host pointers, which are not settled\n"); + Require(!settlesShared || settledAtMap == settledBefore + 1, "sparse import: the 1 MiB dma-buf import made at map time was not settled on the settle queue"); const auto boundary = ChunkBytes; std::memset(mapping + boundary - 65536, 0x11, 131072); const auto ranges = Register(mapping, bytes); const auto address = reinterpret_cast(mapping); + const auto settledSmall = AgcDriver::Graphics::HostImportsSettled(); const bool atMap = device->ImportGuestMemory(ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); Require(Imported(device->Device(), address, static_cast(bytes)), "sparse import: a range larger than udmabuf's size limit was not imported"); Require(atMap, "sparse import: the import made without the GPU lock was refused"); + Require(!settlesShared || AgcDriver::Graphics::HostImportsSettled() == settledSmall + 1, "sparse import: the sparse dma-buf import made at map time was not settled on the settle queue"); bool filled = false; { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index 758781d128..09c9c0b2da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -51,7 +51,7 @@ std::array TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface) { +std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { const auto address = base + surface * Stride; std::vector userData(16, 0u); const auto texture = TextureDescriptor(address); @@ -69,6 +69,7 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); + if (!flush) return StorageTexture::FindLive(address, SurfaceBytes); StorageTexture::FlushPending(address, static_cast(SurfaceBytes), nullptr, "test"); device.WaitIdle(); return StorageTexture::FindLive(address, SurfaceBytes); @@ -126,6 +127,36 @@ int main() { Require(cached == (surface >= 2u), "surface " + std::to_string(surface) + (cached ? " is still cached" : " was evicted") + " after " + std::to_string(capacity + 2u) + " surfaces under a budget of " + std::to_string(capacity) + " of them"); Require(cached == !images[surface].expired(), "surface " + std::to_string(surface) + (cached ? " is cached but its weak image expired" : " was evicted but its resource cache image is still alive")); } + const auto released = AgcDriver::Graphics::ReleaseTextureCaches(held); + Require(released >= held, "a release of one image's bytes freed " + std::to_string(released >> 20u) + " MiB, less than the " + std::to_string(held >> 20u) + " MiB of the oldest image"); + Require(StorageTexture::FindLive(base + 2u * Stride, SurfaceBytes) == nullptr && images[2].expired(), "the oldest cached storage image survived a release of its bytes"); + Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr && !images[3].expired(), "a release of one image's bytes evicted more than the oldest image"); + Require(StorageTexture::FindLive(base + (capacity + 1u) * Stride, SurfaceBytes) != nullptr, "a release of one image's bytes evicted the newest image"); + { + const auto dirty = Store(*device, base, 3, false); + Require(dirty != nullptr && dirty->Dirty(), "a store without a flush left no pending results on surface 3"); + Require(Store(*device, base, 4) != nullptr, "surface 4 is not cached"); + for (std::uint32_t surface = 5; surface < capacity + 2u; ++surface) Require(Store(*device, base, surface) != nullptr, "surface " + std::to_string(surface) + " is not cached"); + } + Require(AgcDriver::Graphics::ReleaseTextureCaches(held) >= held, "a release with a dirty oldest image freed nothing"); + Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr, "a release took the dirty oldest image before a clean one"); + Require(StorageTexture::FindLive(base + 4u * Stride, SurfaceBytes) == nullptr && images[4].expired(), "a release did not take the oldest clean image"); + const auto rest = AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); + Require(rest >= held * (capacity - 3u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 3u)) >> 20u) + " MiB cached"); + device->WaitIdle(); + for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) { + if (surface == 3u) continue; + Require(StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) == nullptr && images[surface].expired(), "surface " + std::to_string(surface) + " survived a release of everything"); + } + { + const auto kept = StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes); + Require(kept != nullptr && kept->Dirty(), "a release of everything took the image with pending results"); + } + StorageTexture::FlushPending(base + 3u * Stride, static_cast(SurfaceBytes), nullptr, "test"); + device->WaitIdle(); + Require(AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}) >= held, "a release did not take the image once its results were stored"); + Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) == nullptr, "surface 3 survived a release after its results were stored"); + Require(AgcDriver::Graphics::ReleaseTextureCaches(held) == 0, "a release from empty caches reported freed bytes"); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 3431424ec1..919662cc42 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,10 @@ alignas(256) constexpr std::array FillCode{ 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, }; +alignas(256) constexpr std::array ReadCode{ + 0x7e020280, 0x7e04020c, 0xf0001108, 0x00010300, 0xbf8c3f70, 0x34080082, 0xe0701000, 0x80000304, 0xbf810000, +}; + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -120,6 +125,20 @@ void Fill(const std::uint32_t* texels) { Dispatch(FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); } +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void ReadImage(const std::uint32_t* texels, std::uint32_t* output) { + std::array userData{}; + const auto buffer = BufferDescriptor(output, SurfaceBytes); + const auto texture = TextureDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(ReadCode, 32, static_cast(userData.size()) << 1u, userData, 1); +} + class Surface { public: explicit Surface(bool watched = false, std::size_t lead = 0) : watched(watched), regionBytes(lead == 0 ? SurfaceBytes : SurfaceBytes + Block) { @@ -202,7 +221,8 @@ 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; + const auto live = StorageTexture::FindLive(surface.Address(), SurfaceBytes); + if (live != nullptr && StorageTexture::FindPending(surface.Address(), SurfaceBytes) == live) 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"); @@ -217,7 +237,35 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } -enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied, NeighbourLeftWatch }; +void PendingIdentity() { + Surface pending; + Surface completed; + std::shared_ptr image; + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; + Store(pending.Texels()); + AgcDriverWaitIdle_nid_postfix(); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + image = StorageTexture::FindLive(pending.Address(), SurfaceBytes); + Require(image != nullptr, "store over untracked: the identity test has no live image"); + StorageTexture::FlushAllPending("pending identity fixture"); + image->MarkDirty(); + Require(StorageTexture::FindPending(pending.Address(), SurfaceBytes) == image, "store over untracked: the identity test did not make its image pending"); + Require(StorageTexture::FindLive(completed.Address(), SurfaceBytes) == nullptr && StorageTexture::FindPending(completed.Address(), SurfaceBytes) == nullptr, "store over untracked: the completed surface unexpectedly has an image"); + hold.unlock(); + Require(HoldPending(hold, pending, Stored), "store over untracked: the wait ignored its own pending image"); + Require(hold.owns_lock(), "store over untracked: the pending image was not held under the GPU mutex"); + std::fill(completed.Texels(), completed.Texels() + SurfaceBytes / 4u, Stored); + hold.unlock(); + const bool held = HoldPending(hold, completed, Stored); + if (!hold.owns_lock()) hold.lock(); + StorageTexture::FlushAllPending("pending identity fixture cleanup"); + hold.unlock(); + Require(!held, "store over untracked: the wait accepted another surface's pending image"); +} + +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied, NeighbourLeftWatch, UntrackedRepeat }; // One 64 KiB block of the surface: the image of its last block, and the source copied into it. constexpr std::uint32_t BlockSide = 128; @@ -252,8 +300,8 @@ Outcome RunSecondStore(const char* name) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (true) { hold.lock(); - const std::array, 1> range{{{address, address + SurfaceBytes}}}; - if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) return true; + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + if (image != nullptr && StorageTexture::FindPending(address, SurfaceBytes) == image) 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"); @@ -294,6 +342,7 @@ Outcome RunSecondStore(const char* name) { } Outcome Run(const Case& test) { + std::optional output; std::optional source; if (test.kind == Kind::Copied || test.kind == Kind::NeighbourCopied) source.emplace(); const bool neighbour = test.kind == Kind::NeighbourLeftWatch; @@ -313,6 +362,16 @@ Outcome Run(const Case& test) { Settle(hold, *source, Filled); } Store(surface.Texels()); + if (test.kind == Kind::UntrackedRepeat) { + Settle(hold, surface, Stored); + output.emplace(); + ReadImage(surface.Texels(), output->Texels()); + hold.lock(); + const auto image = StorageTexture::FindLive(surface.Address(), SurfaceBytes); + Require(image != nullptr, "store over untracked: the sampled surface has no live storage image"); + image->MarkDirty(); + hold.unlock(); + } if (test.kind == Kind::Filled || leaves) { Settle(hold, surface, Stored); if (leaves) { @@ -355,16 +414,14 @@ Outcome Run(const Case& test) { std::fill(surface.Texels(), surface.Texels() + SurfaceBytes / 4u, Untouched); Store(surface.Texels()); // Pending results are the surface image's alone once it exists. + const std::array, 1> range{{{address, address + SurfaceBytes}}}; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (true) { hold.lock(); - if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr) { - if (StorageTexture::AnyPending()) break; - hold.unlock(); - return Outcome::Missed; - } + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) break; hold.unlock(); - Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's storage image was never created"); + if (Holds(surface.Texels(), Stored)) return Outcome::Missed; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's result neither became pending nor reached guest memory"); std::this_thread::yield(); } const auto from = StorageTexture::FindLive(source->Address(), Block); @@ -415,8 +472,9 @@ Outcome Run(const Case& test) { } -int main() { +int main(int argc, char** argv) { try { + Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--pending-identity"), "store over untracked: unknown test argument"); { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -424,11 +482,17 @@ int main() { struct ShutdownGuard { ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } } shutdown; + if (argc == 2) { + PendingIdentity(); + AgcDriverShutdown_nid_postfix(); + std::puts("pending surface identity tests passed"); + return 0; + } { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); } - constexpr std::array cases{{ + constexpr std::array cases{{ {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, {"a fill of the surface", Kind::Filled, 0xe000}, @@ -438,6 +502,7 @@ int main() { {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, {"an image copy into another block of the surface", Kind::NeighbourCopied, 0xe000}, {"a surface whose first block left the write watch with the page before it", Kind::NeighbourLeftWatch, 0xe000}, + {"untracked storage results written again after an image read", Kind::UntrackedRepeat, 0xe000}, }}; bool kept = true; for (const auto& test : cases) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp new file mode 100644 index 0000000000..ebc36d2016 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp @@ -0,0 +1,183 @@ +#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 ValueCount = 16; +constexpr std::uint32_t Threads = ValueCount * ValueCount; +constexpr std::uint32_t Groups = Threads / 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = { + 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x83ffu, 0x0400u, 0x8400u, 0x3c00u, 0xbc00u, 0x7c00u, 0xfc00u, 0x7e00u, 0x7c01u, 0xfe00u, 0x7bffu, +}; + +alignas(256) constexpr std::array CompareCode{ + 0x34020084u, 0x34040086u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0x7d940b04u, 0xd501000au, 0x01a90280u, + 0x7d9c0b04u, 0xd501000bu, 0x01a90280u, 0x7d980b04u, 0xd501000cu, 0x01a90280u, 0x7d960b04u, 0xd501000du, + 0x01a90280u, 0x7d9a0b04u, 0xd501000eu, 0x01a90280u, 0x7d920b04u, 0xd501000fu, 0x01a90280u, 0x7dda0b04u, + 0xd5010010u, 0x01a90280u, 0x7dd20b04u, 0xd5010011u, 0x01a90280u, 0x7dd60b04u, 0xd5010012u, 0x01a90280u, + 0x7dd80b04u, 0xd5010013u, 0x01a90280u, 0x7dd40b04u, 0xd5010014u, 0x01a90280u, 0x7ddc0b04u, 0xd5010015u, + 0x01a90280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +alignas(256) constexpr std::array CompareExecCode{ + 0x34020084u, 0x34040086u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7db40b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000au, 0x00550280u, 0xbe94037eu, 0x7dbc0b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000bu, + 0x00550280u, 0xbe94037eu, 0x7db80b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000cu, 0x00550280u, 0xbe94037eu, + 0x7db60b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000du, 0x00550280u, 0xbe94037eu, 0x7dba0b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000eu, 0x00550280u, 0xbe94037eu, 0x7db20b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000fu, + 0x00550280u, 0xbe94037eu, 0x7dfa0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010010u, 0x00550280u, 0xbe94037eu, + 0x7df20b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010011u, 0x00550280u, 0xbe94037eu, 0x7df60b04u, 0xbe95037eu, + 0xbefe0314u, 0xd5010012u, 0x00550280u, 0xbe94037eu, 0x7df80b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010013u, + 0x00550280u, 0xbe94037eu, 0x7df40b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010014u, 0x00550280u, 0xbe94037eu, + 0x7dfc0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010015u, 0x00550280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, + 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t inputsPreserved[Groups]; + std::uint32_t inputsFlushed[Groups]; +}; + +constexpr Row Rows[] = { + {"eq", + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", + {0x7aab7aabu, 0x7aa87aafu, 0x7aa07abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}, + {0x7abf7abfu, 0x7abf7abfu, 0x7abf7abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}}, +}; + +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%04x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, ShaderRecompiler::ShaderFloatMode mode) { + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + Input[pair * Inputs] = Values[pair / ValueCount]; + Input[pair * Inputs + 1u] = Values[pair % ValueCount]; + Input[pair * Inputs + 2u] = 0u; + Input[pair * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* form, const char* modeName, bool flushed) { + for (std::uint32_t index = 0; index < std::size(Rows); ++index) { + const Row& row = Rows[index]; + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + const std::uint32_t actual = Output[pair * Results + index]; + const std::uint32_t expected = ((flushed ? row.inputsFlushed : row.inputsPreserved)[pair / 32u] >> (pair % 32u)) & 1u; + Require(actual == expected, std::string(form) + row.condition + "_f16 " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + Hex(actual) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode mode; + bool flushed; + }; + const Mode modes[] = { + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false}, + }; + for (const Mode& mode : modes) { + Run(*device, CompareCode, mode.mode); + Check("v_cmp_", mode.name, mode.flushed); + Run(*device, CompareExecCode, mode.mode); + Check("v_cmpx_", mode.name, mode.flushed); + } + std::puts("v_cmp and v_cmpx f16 denormal mode 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/VCmpF32DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32DenormalModes.cpp new file mode 100644 index 0000000000..4e23992b13 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32DenormalModes.cpp @@ -0,0 +1,180 @@ +#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 ValueCount = 16; +constexpr std::uint32_t Threads = ValueCount * ValueCount; +constexpr std::uint32_t Groups = Threads / 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = { + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000001u, 0x007fffffu, 0x807fffffu, 0x00800000u, 0x80800000u, + 0x3f800000u, 0xbf800000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800001u, 0xffc00000u, 0x7f7fffffu, +}; + +alignas(256) constexpr std::array CompareCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7c040b04u, 0xd501000au, 0x01a90280u, + 0x7c0c0b04u, 0xd501000bu, 0x01a90280u, 0x7c080b04u, 0xd501000cu, 0x01a90280u, 0x7c060b04u, 0xd501000du, + 0x01a90280u, 0x7c0a0b04u, 0xd501000eu, 0x01a90280u, 0x7c020b04u, 0xd501000fu, 0x01a90280u, 0x7c1a0b04u, + 0xd5010010u, 0x01a90280u, 0x7c120b04u, 0xd5010011u, 0x01a90280u, 0x7c160b04u, 0xd5010012u, 0x01a90280u, + 0x7c180b04u, 0xd5010013u, 0x01a90280u, 0x7c140b04u, 0xd5010014u, 0x01a90280u, 0x7c1c0b04u, 0xd5010015u, + 0x01a90280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +alignas(256) constexpr std::array CompareExecCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7c240b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000au, 0x00550280u, 0xbe94037eu, 0x7c2c0b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000bu, + 0x00550280u, 0xbe94037eu, 0x7c280b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000cu, 0x00550280u, 0xbe94037eu, + 0x7c260b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000du, 0x00550280u, 0xbe94037eu, 0x7c2a0b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000eu, 0x00550280u, 0xbe94037eu, 0x7c220b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000fu, + 0x00550280u, 0xbe94037eu, 0x7c3a0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010010u, 0x00550280u, 0xbe94037eu, + 0x7c320b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010011u, 0x00550280u, 0xbe94037eu, 0x7c360b04u, 0xbe95037eu, + 0xbefe0314u, 0xd5010012u, 0x00550280u, 0xbe94037eu, 0x7c380b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010013u, + 0x00550280u, 0xbe94037eu, 0x7c340b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010014u, 0x00550280u, 0xbe94037eu, + 0x7c3c0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010015u, 0x00550280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, + 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t inputsPreserved[Groups]; + std::uint32_t inputsFlushed[Groups]; +}; + +constexpr Row Rows[] = { + {"eq", + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", + {0x7aab7aabu, 0x7aa87aafu, 0x7aa07abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}, + {0x7abf7abfu, 0x7abf7abfu, 0x7abf7abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, ShaderRecompiler::ShaderFloatMode mode) { + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + Input[pair * Inputs] = Values[pair / ValueCount]; + Input[pair * Inputs + 1u] = Values[pair % ValueCount]; + Input[pair * Inputs + 2u] = 0u; + Input[pair * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* form, const char* modeName, bool flushed) { + for (std::uint32_t index = 0; index < std::size(Rows); ++index) { + const Row& row = Rows[index]; + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + const std::uint32_t actual = Output[pair * Results + index]; + const std::uint32_t expected = ((flushed ? row.inputsFlushed : row.inputsPreserved)[pair / 32u] >> (pair % 32u)) & 1u; + Require(actual == expected, std::string(form) + row.condition + "_f32 " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + Hex(actual) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode mode; + bool flushed; + }; + const Mode modes[] = { + {"denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xd0u, true, true, false}, false}, + {"denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xd0u, true, false, false}, false}, + {"denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xe0u, true, true, false}, true}, + {"denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xe0u, true, false, false}, true}, + }; + for (const Mode& mode : modes) { + Run(*device, CompareCode, mode.mode); + Check("v_cmp_", mode.name, mode.flushed); + Run(*device, CompareExecCode, mode.mode); + Check("v_cmpx_", mode.name, mode.flushed); + } + std::puts("v_cmp and v_cmpx f32 denormal mode 1 and 2 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/VectorWindowNarrowing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp new file mode 100644 index 0000000000..73b15eb93a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 6; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020086, 0x34060088, 0x4a140680, 0x4a160681, 0x4a180682, 0x4a1a0683, 0x4a1c0684, 0x4a1e0685, + 0x4a200686, 0x4a220687, 0x4a240688, 0x4a260689, 0x4a28068a, 0x4a2a068b, 0x4a2c068c, 0x4a2e068d, + 0x4a30068e, 0x4a32068f, 0x4a340690, 0x4a360691, 0x4a380692, 0x4a3a0693, 0x4a3c0694, 0x4a3e0695, + 0x4a400696, 0x4a420697, 0x4a440698, 0x4a460699, 0x4a48069a, 0x4a4a069b, 0x4a4c069c, 0x4a4e069d, + 0x4a50069e, 0x4a52069f, 0x877c8308, 0x7e08870a, 0xbefc0309, 0x7e0a870a, 0x7e148504, 0x877c8308, + 0x7e148505, 0xe0701000, 0x80010401, 0xe0701004, 0x80010501, 0xe0701008, 0x80010c01, 0xe070100c, + 0x80010d01, 0xe0701010, 0x80011e01, 0xe0701014, 0x80012901, 0xbf810000, +}; + +constexpr const char* Names[Checked] = {"v4", "v5", "v12", "v13", "v30", "v41"}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FunctionVariables(std::span words) { + std::uint32_t count = 0; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + if (length == 0u) break; + if ((words[at] & 0xffffu) == spv::OpVariable && at + 3u < words.size() && words[at + 3u] == spv::StorageClassFunction) ++count; + at += length; + } + return count; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t bounded, std::uint32_t data) { + Output.fill(0xdeadbeefu); + std::vector userData(10, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[8] = bounded; + userData[9] = data; + 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); + Require(FunctionVariables(result.spirv.Words()) > 8u, "vector window narrowing: wave" + std::to_string(waveSize) + " window was not split into separate slot variables"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t bounded, std::uint32_t data) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + std::array window{}; + for (std::uint32_t k = 0; k < window.size(); ++k) window[k] = (tid << 8u) + k; + const std::uint32_t v4 = window[bounded & 3u]; + const std::uint32_t v5 = window[data]; + window[data] = v4; + window[bounded & 3u] = v5; + const std::array expected{v4, v5, window[2], window[3], window[20], window[31]}; + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t index = 0; index < Checked; ++index) { + Require(out[index] == expected[index], "vector window narrowing: wave" + std::to_string(waveSize) + " data index " + std::to_string(data) + " 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; + } + for (const std::uint32_t data : {20u, 2u, 31u}) { + Run(*device, 32, 32, 7u, data); + Check(32, 32, 7u, data); + Run(*device, 64, 64, 7u, data); + Check(64, 64, 7u, data); + } + std::puts("vector window narrowing 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 index 5b5a66615f..08adb9fde4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -129,9 +129,10 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro pixel.targetOutputMode[0] = 9; pixel.targetExportMapping.fill(0xe4u); const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + const std::vector pixelUserData(4, 0u); ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, - {waveSize, 0, std::vector(4, 0u), std::nullopt, pixel, std::nullopt, pixelMemory}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, device.Target(), PixelPushLayout(vertexPush, device.Target()) }; diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 524575eefd..2383684946 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -5,6 +5,7 @@ #include "ControlFlow/Structurizer.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "Optimization/RequestMemoryView.hpp" #include "Recompiler.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include namespace { @@ -42,6 +44,7 @@ bool g_assembly = false; bool g_memory = false; bool g_spirv = false; bool g_prepare = false; +bool g_materialize = false; bool g_graph = false; bool g_maintenance8 = false; bool g_code = false; @@ -106,21 +109,37 @@ bool Replay(const char* path) { return false; } } - try { - auto program = ShaderRecompiler::PrepareResourceProgram(request.request); - constexpr ShaderRecompiler::ResourceMaterializer materializer; - static_cast(materializer.ExtractPlan(program)); - } catch (const std::exception& error) { - std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str()); - return false; + if (g_materialize && g_maintenance8) request.request.target.nonConstantImageOffsets = true; + if (!g_materialize) { + try { + auto program = ShaderRecompiler::PrepareResourceProgram(request.request); + constexpr ShaderRecompiler::ResourceMaterializer materializer; + static_cast(materializer.ExtractPlan(program)); + } catch (const std::exception& error) { + std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str()); + return false; + } } - if (g_prepare) { + if (g_prepare || g_materialize) { + const char* phase = "prepare"; try { const auto handle = ShaderRecompiler::PrepareShader(request.request); - std::printf(" prepared: %zu SPIR-V words\n", ShaderRecompiler::GetPreparedArtifact(*handle).spirv.Words().size()); + std::printf(" prepared: %zu SPIR-V words\n", ShaderRecompiler::GetPreparedArtifact(*handle).spirv.size()); + if (g_materialize) { + phase = "prepared invocation"; + const auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(request.request, handle); + if (!invocation.has_value()) throw std::runtime_error("request does not match its prepared shader"); + ShaderRecompiler::RequestMemoryView memory(request.request.context.memory); + const auto runtime = memory.MakeRuntime(request.request.context.userData, request.request.shader.codeAddress); + phase = "capture"; + const auto capture = invocation->Capture(runtime); + phase = "materialize"; + const auto result = invocation->Materialize(*capture); + std::printf(" materialized: %zu SPIR-V words\n", result->spirv.size()); + } return true; } catch (const std::exception& error) { - std::printf(" prepare failed: %s\n", FirstLine(error.what()).c_str()); + std::printf(" %s failed: %s\n", phase, FirstLine(error.what()).c_str()); return false; } } @@ -147,7 +166,7 @@ bool Replay(const char* path) { if (g_disassemble) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string text; - if (tools.Disassemble(result.spirv, &text, SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) std::printf("%s\n", text.c_str()); + if (tools.Disassemble(result.spirv.data(), result.spirv.size(), &text, SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) std::printf("%s\n", text.c_str()); } #else if (g_disassemble) std::printf(" (--dis needs ANYPS5_ENABLE_SPIRV_TOOLS=ON)\n"); @@ -163,7 +182,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -196,6 +215,10 @@ int main(int argc, char** argv) { g_prepare = true; continue; } + if (std::string(argv[i]) == "--materialize") { + g_materialize = true; + continue; + } if (std::string(argv[i]) == "--spv") { g_spirv = true; continue; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 482edbfdaf..29fe39434e 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -1205,9 +1205,10 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, count -= skip; } if (instance.gapless.totalSamples != 0) { - const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + const std::uint64_t remaining = instance.gapless.totalSamples > instance.segmentSamples ? instance.gapless.totalSamples - instance.segmentSamples : 0; count = static_cast(std::min(count, remaining)); } + instance.segmentSamples += count; instance.totalDecodedSamples += count; if (Resampling(instance)) { std::vector values(count * channels); @@ -1620,12 +1621,16 @@ int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t fl created->initialized = true; } if (codec == CODEC_M4AAC) CheckAacFlags(flags); - const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); std::lock_guard lock(g_lock); - g_instances[id] = std::move(created); - *instance = id; - AJM_TRACE("[ajm] instance create: context %u, codec %u, flags 0x%llx -> instance %u\n", context, codec, static_cast(flags), id); - return 0; + for (std::uint32_t attempt = 0; attempt < 0x4000; ++attempt) { + const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); + if (g_instances.contains(id)) continue; + g_instances.emplace(id, std::move(created)); + *instance = id; + AJM_TRACE("[ajm] instance create: context %u, codec %u, flags 0x%llx -> instance %u\n", context, codec, static_cast(flags), id); + return 0; + } + return SCE_AJM_ERROR_OUT_OF_RESOURCES; } int APS5_VABI sceAjmInstanceDestroy(uint32_t context, uint32_t instance) { diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fba6d9efbe..88f264c00a 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -271,6 +271,7 @@ 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); +[[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); [[noreturn]] void ThrowCppError_nid_no_patch(int code); @@ -994,9 +995,8 @@ int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); } -int APS5_VABI __cxa_call_unexpected_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +[[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException) { + __cxa_call_terminate_nid_postfix(unwindException); } APS5_EXPORT("0hlfW1O4Aa4", sceAmprLocaleconv); diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index c3b44b3d75..be7cad24d9 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -184,8 +184,9 @@ int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnified return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentGetAddcontDownloadProgress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, void* progress) { + (void)service_label; + if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } } diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index b366da3067..47489f9d5e 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,8 +1,10 @@ +#include #include #include #include #include #include +#include #include #include #include @@ -113,6 +115,25 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve return 0; } +int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t port_id, uint32_t* attributes, uint32_t* num_attributes) { + std::lock_guard lock(g_mutex); + if (num_attributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + constexpr std::uint32_t supported[]{ + AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE, + }; + if (attributes == nullptr) { + *num_attributes = std::size(supported); + return 0; + } + const std::uint32_t count = std::min(*num_attributes, std::size(supported)); + std::copy_n(std::begin(supported), count, attributes); + *num_attributes = count; + return 0; +} + int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, uint32_t* id) { std::lock_guard lock(g_mutex); if (!g_initialized) return AUDIO3D_ERROR_NOT_READY; diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 2e6e7995d9..0da434395f 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -20,6 +20,15 @@ configure_windows_unwind(libSceAudioOut) if(BUILD_TESTING) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(audio_out_port_locks_tests tests/AudioOutPortLocks.cpp) + target_include_directories(audio_out_port_locks_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_port_locks_tests PRIVATE libSceAudioOut libc) + set_target_properties(audio_out_port_locks_tests PROPERTIES ENABLE_EXPORTS ON) + add_test(NAME audio_out_independent_output COMMAND audio_out_port_locks_tests output) + add_test(NAME audio_out_independent_close COMMAND audio_out_port_locks_tests close) + add_test(NAME audio_out_same_port_close COMMAND audio_out_port_locks_tests same-close) + set_tests_properties(audio_out_independent_output audio_out_independent_close audio_out_same_port_close PROPERTIES TIMEOUT 10) + add_test_executable(audio_out_resampling_tests tests/AudioOutResampling.cpp) target_include_directories(audio_out_resampling_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(audio_out_resampling_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index dd844adea9..5f6d3a37b4 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -1,5 +1,3 @@ -#include -#include #include #include #include @@ -110,7 +108,7 @@ struct Port { }; static std::mutex g_mutex; -static std::mutex g_outputMutex; +static std::array g_outputMutexes; static Port g_ports[PORTS_MAX]; static bool g_sdlInitialized = false; @@ -351,7 +349,8 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len } int APS5_VABI sceAudioOutClose(int handle) { - std::lock_guard outputLock(g_outputMutex); + if (handle <= 0 || handle > PORTS_MAX) return -2144993277; + std::lock_guard outputLock(g_outputMutexes[handle - 1]); std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { @@ -364,7 +363,8 @@ int APS5_VABI sceAudioOutClose(int handle) { int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { const AudioOutCall call; - std::lock_guard outputLock(g_outputMutex); + if (handle <= 0 || handle > PORTS_MAX) return -2144993277; + std::lock_guard outputLock(g_outputMutexes[handle - 1]); std::unique_lock lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { @@ -383,12 +383,20 @@ int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { } int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) try { + const AudioOutCall call; if (param == nullptr || num == 0) { return -2144993276; } - const AudioOutCall call; - std::lock_guard outputLock(g_outputMutex); + std::array selected{}; + for (std::uint32_t i = 0; i < num; ++i) { + if (param[i].handle <= 0 || param[i].handle > PORTS_MAX) return -2144993277; + selected[param[i].handle - 1] = true; + } + std::array, PORTS_MAX> outputLocks; + for (int index = 0; index < PORTS_MAX; ++index) { + if (selected[index]) outputLocks[index] = std::unique_lock(g_outputMutexes[index]); + } std::unique_lock lock(g_mutex); for (std::uint32_t i = 0; i < num; i++) { @@ -551,7 +559,10 @@ int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { } int APS5_VABI sceAudioOutExClose(int handle) { - std::lock_guard outputLock(g_outputMutex); + if (handle <= 0 || handle > PORTS_MAX) { + throw std::runtime_error("sceAudioOutExClose: handle " + std::to_string(handle) + " is not an open Ex port"); + } + std::lock_guard outputLock(g_outputMutexes[handle - 1]); std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr || port->type != PORT_TYPE_EX) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 74a4a2b1c4..e11ee633e5 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -23,7 +23,6 @@ using Clock = std::chrono::steady_clock; static constexpr std::size_t CONTEXT_MEMORY = 0x10000; static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16; -static constexpr std::size_t ADVANCED_GRAINS_MAX = 16; // Pushes, polls and advances traced individually before the trace falls back to the per-second summary. static constexpr std::uint64_t CALL_TRACE_FULL = 16; // Grains the device waits for before it starts playing (and again whenever it ran dry), so the @@ -264,7 +263,6 @@ int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); if (context->device != 0) { - if (context->advancedGrains.size() >= ADVANCED_GRAINS_MAX) throw std::runtime_error("AudioOut2: more than 16 completed grains await a push"); if (!context->advancedMode) { while (!context->pendingGrains.empty()) { Render(*context, context->pendingGrains.front()); @@ -323,7 +321,7 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); - const auto level = QueueLevel(*context, Clock::now()); + const auto level = std::min(context->queueDepth, QueueLevel(*context, Clock::now()) + static_cast(context->advancedGrains.size())); if (queue_level) *queue_level = level; if (available_queue) *available_queue = context->queueDepth - level; context->queueLevelPolls++; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index cac79482e3..b1d5725eca 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" +#include #include #include #include @@ -21,6 +22,7 @@ int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); @@ -54,7 +56,7 @@ void SetData(AudioOut2PortHandle port, const void* data) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector Play(const std::function& feed, bool advanced = false) { +std::vector Play(const std::function& feed, bool advanced = false, std::uint32_t queueDepth = 1) { const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_buffers-" + std::to_string(std::random_device{}()) + ".raw"); std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); @@ -62,7 +64,7 @@ std::vector Play(const std::function> expected; + for (std::uint32_t index = 0; index < 64; ++index) expected.push_back(Grain(0.001f * static_cast(index + 1))); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + std::thread producer([&] { + auto buffer = Grain(0.0f); + for (const auto& data : expected) { + buffer = data; + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + } + }); + producer.join(); + for (std::size_t index = 0; index < expected.size(); ++index) Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + std::vector*> grains; + for (const auto& data : expected) grains.push_back(&data); + RequirePlayed(played, grains); +} + void TestPushingThreadWaitsForAdvance() { const auto first = Grain(0.01f); const auto second = Grain(0.02f); @@ -232,16 +254,50 @@ void TestPushingThreadWaitsForAdvance() { RequirePlayed(played, {&first, &second}); } +void TestRenderingSizedByLevelStaysWithinQueueDepth() { + constexpr std::uint32_t queueDepth = 4; + constexpr std::uint32_t grains = 48; + std::vector> expected; + for (std::uint32_t index = 0; index < grains; index++) expected.push_back(Grain(0.001f * static_cast(index + 1))); + std::uint32_t mostAwaiting = 0; + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + std::uint32_t advanced = 0; + std::uint32_t pushed = 0; + while (pushed < grains) { + std::uint32_t available = 0; + Require(sceAudioOut2ContextGetQueueLevel(context, nullptr, &available) == 0); + for (std::uint32_t index = 0; index < available && advanced < grains; index++) { + SetData(port, expected[advanced].data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + advanced++; + } + mostAwaiting = std::max(mostAwaiting, advanced - pushed); + if (advanced == pushed) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + Require(sceAudioOut2ContextPush(context, 1) == 0); + pushed++; + } + }, true, queueDepth); + Require(mostAwaiting <= queueDepth); + std::vector*> order; + for (const auto& data : expected) order.push_back(&data); + RequirePlayed(played, order); +} + } int main() { SetEnvironment("SDL_AUDIODRIVER", "disk"); TestAdvanceRetainsRewrittenBuffer(); TestAdvanceRetainsVolume(); + TestAdvanceBurstRetainsEveryGrain(); TestPushingThreadWaitsForAdvance(); TestBuffersSetBeforeOnePushPlayInOrder(); TestBurstThenGapKeepsEveryBuffer(); TestAdvancingPortWithoutNewBufferPlaysSilence(); TestRewrittenBufferRepeats(); + TestRenderingSizedByLevelStaysWithinQueueDepth(); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp index c06cc14d1d..e6cdeed0d7 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp @@ -156,6 +156,48 @@ void TestErrors() { Require(destination == untouched, "a rejected call must not write the destination"); } +void TestIndependentPortOutputDuringWait(bool batch) { + const std::vector longBlock(frequency, 256); + const std::vector shortBlock(frames, 256); + const int waiting = sceAudioOutOpen(user, portTypeVibration, 0, frequency, frequency, formatS16Mono); + const int independent = Open(portTypeVibration); + Require(waiting > 0, "waiting port must open"); + Require(sceAudioOutOutput(waiting, longBlock.data()) == static_cast(frequency), "initial output failed"); + std::atomic started = false; + std::thread output([&] { + started = true; + AudioOutOutputParam param{waiting, longBlock.data()}; + const int result = batch ? sceAudioOutOutputs(¶m, 1) : sceAudioOutOutput(waiting, longBlock.data()); + Require(result == static_cast(frequency), "paced output failed"); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + const auto before = std::chrono::steady_clock::now(); + Require(sceAudioOutOutput(independent, shortBlock.data()) == static_cast(frames), "independent output failed"); + const auto elapsed = std::chrono::steady_clock::now() - before; + output.join(); + Require(sceAudioOutClose(waiting) == 0 && sceAudioOutClose(independent) == 0, "ports must close"); + std::fprintf(stderr, "Independent port output waited %lld ms\n", static_cast(std::chrono::duration_cast(elapsed).count())); + Require(elapsed < std::chrono::milliseconds(100), "pacing one audio port must not block output on another port"); +} + +void TestOverlappingOutputBatches() { + const std::vector block(frames, 256); + const int first = Open(portTypeVibration); + const int second = Open(portTypeVibration); + AudioOutOutputParam duplicate[2] = {{first, block.data()}, {first, block.data()}}; + Require(sceAudioOutOutputs(duplicate, 2) == static_cast(frames), "duplicate handles in a batch must not deadlock"); + const auto output = [&](bool reverse) { + AudioOutOutputParam params[2] = {{reverse ? second : first, block.data()}, {reverse ? first : second, block.data()}}; + for (int i = 0; i < 12; ++i) Require(sceAudioOutOutputs(params, 2) == static_cast(frames), "overlapping output batch failed"); + }; + std::thread forward(output, false); + std::thread reverse(output, true); + forward.join(); + reverse.join(); + Require(sceAudioOutClose(first) == 0 && sceAudioOutClose(second) == 0, "batch ports must close"); +} + void TestQueriesDuringOutput(bool batch, bool drain) { const std::vector block(frequency, 256); const int handle = sceAudioOutOpen(user, portTypeVibration, 0, frequency, frequency, formatS16Mono); @@ -196,6 +238,9 @@ int main() { TestReopenedPort(); TestErrors(); TestPortWithoutDeviceKeepsRealTime(); + TestIndependentPortOutputDuringWait(false); + TestIndependentPortOutputDuringWait(true); + TestOverlappingOutputBatches(); TestQueriesDuringOutput(false, false); TestQueriesDuringOutput(false, true); TestQueriesDuringOutput(true, false); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp new file mode 100644 index 0000000000..107a2ef2f5 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp @@ -0,0 +1,135 @@ +#include "SDL.h" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutClose(int); +} + +namespace { + +constexpr std::uint32_t Grain = 256; +std::atomic nextDevice{2}; +std::atomic blocked{false}; +std::atomic released{false}; +std::atomic clockQueries{0}; +std::atomic clears{0}; +std::array, 4> queued{}; +std::array, 4> closed{}; +std::mutex stateMutex; +std::condition_variable changed; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "AudioOut port locks: %s\n", message); + std::abort(); +} + +bool WaitFor(const std::atomic& condition, std::chrono::milliseconds timeout) { + std::unique_lock lock(stateMutex); + return changed.wait_for(lock, timeout, [&] { return condition.load(std::memory_order_acquire); }); +} + +} + +extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime(void) { + clockQueries.fetch_add(1, std::memory_order_relaxed); + return 1; +} + +extern "C" int SDLCALL SDL_InitSubSystem(Uint32) { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID, int) {} + +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char*, int, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + *obtained = *desired; + return nextDevice.fetch_add(1, std::memory_order_relaxed); +} + +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID device) { + Require(device == 2 || device == 3, "unknown device polled"); + if (device == 2 && !released.load(std::memory_order_acquire)) { + blocked.store(true, std::memory_order_release); + changed.notify_all(); + return 1u << 20; + } + return 0; +} + +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID device) { + if (device == 2 && !released.load(std::memory_order_acquire)) { + clears.fetch_add(1, std::memory_order_relaxed); + released.store(true, std::memory_order_release); + } +} + +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID device, const void* data, Uint32 bytes) { + Require(device == 2 || device == 3, "unknown device queued"); + Require(!closed[device].load(std::memory_order_acquire), "output used a closed device"); + Require(bytes == Grain * sizeof(std::int16_t), "output size changed"); + const auto* samples = static_cast(data); + const auto expected = device == 2 ? 1000 : 2000; + for (std::uint32_t i = 0; i < Grain; ++i) Require(samples[i] == expected, "output samples changed"); + queued[device].fetch_add(1, std::memory_order_relaxed); + return 0; +} + +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID device) { + Require(device == 2 || device == 3, "unknown device closed"); + Require(!closed[device].exchange(true, std::memory_order_acq_rel), "device closed twice"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected a test mode"); + const std::string_view mode(argv[1]); + const bool unrelatedOutput = mode == "output"; + const bool unrelatedClose = mode == "close"; + const bool samePortClose = mode == "same-close"; + Require(unrelatedOutput || unrelatedClose || samePortClose, "unknown test mode"); + const int first = sceAudioOutOpen(0, 0, 0, Grain, 48000, 0); + const int second = sceAudioOutOpen(0, 0, 0, Grain, 48000, 0); + Require(first > 0 && second > 0 && first != second, "port creation failed"); + std::array firstSamples; + std::array secondSamples; + firstSamples.fill(1000); + secondSamples.fill(2000); + int firstResult = -1; + int secondResult = -1; + std::atomic secondStarted{false}; + std::atomic secondDone{false}; + std::thread producer([&] { firstResult = sceAudioOutOutput(first, firstSamples.data()); }); + Require(WaitFor(blocked, std::chrono::seconds(2)), "first port did not enter backpressure"); + std::thread other([&] { + secondStarted.store(true, std::memory_order_release); + changed.notify_all(); + secondResult = unrelatedOutput ? sceAudioOutOutput(second, secondSamples.data()) : sceAudioOutClose(samePortClose ? first : second); + secondDone.store(true, std::memory_order_release); + changed.notify_all(); + }); + Require(WaitFor(secondStarted, std::chrono::seconds(2)), "second operation did not start"); + const bool completedWhileBlocked = WaitFor(secondDone, std::chrono::milliseconds(samePortClose ? 150 : 2000)); + released.store(true, std::memory_order_release); + producer.join(); + other.join(); + Require(clockQueries.load() != 0 && clears.load() == 0, "drain timeout invalidated the controlled wait"); + Require(firstResult == static_cast(Grain), "blocked output failed"); + Require(secondResult == (unrelatedOutput ? static_cast(Grain) : 0), "second operation failed"); + Require(completedWhileBlocked != samePortClose, "port output/close lock scope is wrong"); + Require(queued[2].load() == 1 && queued[3].load() == (unrelatedOutput ? 1u : 0u), "unexpected queued output count"); + if (!samePortClose) Require(sceAudioOutClose(first) == 0, "first port close failed"); + if (!unrelatedClose) Require(sceAudioOutClose(second) == 0, "second port close failed"); + Require(closed[2].load() && closed[3].load(), "a device remained open"); + return 0; +} diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index 76722d3554..fc1a113e4f 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -139,7 +139,7 @@ std::int32_t createAac(const AudiodecParamM4aac* param, AudiodecM4aacInfo* info, if (param->ui_max_channels > 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; if (param->ui_enable_heaac > 1) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; const std::uint32_t channels = param->ui_max_channels == 0 ? 2 : param->ui_max_channels; - instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels); + instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels, param->ui_enable_heaac == 1); instance.channels = channels; instance.sampleRate = sampleRate; info->ui_sampling_freq = sampleRate; diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.cpp b/core/libs/prx/libSceAudiodec/src/Codecs.cpp index aca46d88d1..c3324c3b1a 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.cpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.cpp @@ -122,35 +122,103 @@ public: } }; +struct AacLayout { + std::uint32_t objectType; + std::uint32_t samplingIndex; + std::uint32_t channelConfiguration; + bool operator==(const AacLayout&) const = default; +}; + +enum class Sbr { + Implicit, + Absent, + WithoutParametricStereo, +}; + +std::vector audioSpecificConfig(const AacLayout& layout, Sbr sbr, std::uint32_t sbrSampleRate) { + std::vector bytes; + std::size_t bits = 0; + const auto put = [&](std::uint32_t value, int width) { + for (int bit = width - 1; bit >= 0; --bit, ++bits) { + if (bits % 8 == 0) bytes.push_back(0); + bytes.back() |= static_cast((value >> bit & 1) << (7 - bits % 8)); + } + }; + put(layout.objectType, 5); + put(layout.samplingIndex, 4); + put(layout.channelConfiguration, 4); + put(0, 3); + if (sbr == Sbr::Implicit) return bytes; + put(0x2B7, 11); + put(5, 5); + put(sbr == Sbr::WithoutParametricStereo, 1); + if (sbr == Sbr::WithoutParametricStereo) { + const auto* rate = std::find(std::begin(AAC_SAMPLE_RATES), std::end(AAC_SAMPLE_RATES), sbrSampleRate); + if (rate == std::end(AAC_SAMPLE_RATES)) { + put(15, 4); + put(sbrSampleRate, 24); + } else { + put(static_cast(rate - std::begin(AAC_SAMPLE_RATES)), 4); + } + put(0x548, 11); + put(0, 1); + } + return bytes; +} + class AacDecoder final : public FfmpegDecoder { public: - AacDecoder(std::int32_t wordSize, bool adts, std::vector> configs) - : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, configs.empty() ? std::vector{} : configs.front()), adts(adts), configs(std::move(configs)) {} + AacDecoder(std::int32_t wordSize, bool adts, bool highEfficiency, std::vector layouts) + : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, layouts.empty() ? std::vector{} : audioSpecificConfig(layouts.front(), highEfficiency ? Sbr::Implicit : Sbr::Absent, 0)), + adts(adts), highEfficiency(highEfficiency), sbr(highEfficiency ? Sbr::Implicit : Sbr::Absent), layouts(std::move(layouts)) {} DecodeResult Decode(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) override { - std::size_t frameBytes = size; - if (adts) { - if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; - frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); - if (frameBytes < 7) return {DecodeStatus::InvalidData}; - if (frameBytes > size) return {DecodeStatus::PartialInput}; - } - if (configs.size() <= 1) return decodePacket(data, frameBytes, pcm, pcmSize); - for (std::size_t index = 0; index < configs.size(); ++index) { - if (index != 0) reopen(configs[index]); - const DecodeResult result = decodePacket(data, frameBytes, pcm, pcmSize); - if (result.status != DecodeStatus::InvalidData) { - configs = {configs[index]}; - return result; + if (!adts) return decodeAccessUnit(data, size, pcm, pcmSize); + if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; + const auto frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); + const std::size_t headerBytes = (data[1] & 1) != 0 ? 7 : 9; + if (frameBytes <= headerBytes) return {DecodeStatus::InvalidData}; + if (frameBytes > size) return {DecodeStatus::PartialInput}; + const AacLayout layout{(data[2] >> 6) + 1u, data[2] >> 2 & 15u, (data[2] & 1u) << 2 | data[3] >> 6}; + if (layout.channelConfiguration == 0) { + if (!layouts.empty()) { + layouts.clear(); + reopen({}); } + return decodePacket(data, frameBytes, pcm, pcmSize); } - reopen(configs.front()); - return {DecodeStatus::InvalidData}; + if (layouts.empty() || layouts.front() != layout) { + layouts = {layout}; + sbr = highEfficiency ? Sbr::Implicit : Sbr::Absent; + reopen(audioSpecificConfig(layout, sbr, 0)); + } + DecodeResult result = decodeAccessUnit(data + headerBytes, frameBytes - headerBytes, pcm, pcmSize); + result.consumed = frameBytes; + return result; } private: + DecodeResult decodeAccessUnit(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) { + for (std::size_t index = 0; index < layouts.size(); ++index) { + if (index != 0) reopen(audioSpecificConfig(layouts[index], sbr, 0)); + DecodeResult result = decodePacket(data, size, pcm, pcmSize); + if (result.status == DecodeStatus::InvalidData) continue; + layouts = {layouts[index]}; + if (sbr == Sbr::Implicit && layouts.front().channelConfiguration == 1 && result.channels > 1) { + sbr = Sbr::WithoutParametricStereo; + reopen(audioSpecificConfig(layouts.front(), sbr, result.sampleRate)); + result = decodePacket(data, size, pcm, pcmSize); + } + return result; + } + if (layouts.size() > 1) reopen(audioSpecificConfig(layouts.front(), sbr, 0)); + return {DecodeStatus::InvalidData}; + } + bool adts; - std::vector> configs; + bool highEfficiency; + Sbr sbr; + std::vector layouts; }; class At9Decoder final : public Decoder { @@ -239,20 +307,16 @@ std::unique_ptr CreateMp3(std::int32_t wordSize) { return std::make_unique(wordSize); } -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels) { - std::vector> configs; +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency) { + std::vector layouts; if (!adts) { const std::uint32_t lowComplexity = 2; - const auto config = [&](std::uint32_t channelConfiguration) { - return std::vector{static_cast(lowComplexity << 3 | samplingFreqIndex >> 1), - static_cast((samplingFreqIndex & 1) << 7 | channelConfiguration << 3)}; - }; - configs.push_back(config(channels < 2 ? 1 : 2)); + layouts.push_back({lowComplexity, samplingFreqIndex, channels < 2 ? 1u : 2u}); for (std::uint32_t channelConfiguration = 3; channelConfiguration <= 7; ++channelConfiguration) { - if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) configs.push_back(config(channelConfiguration)); + if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) layouts.push_back({lowComplexity, samplingFreqIndex, channelConfiguration}); } } - return std::make_unique(wordSize, adts, std::move(configs)); + return std::make_unique(wordSize, adts, highEfficiency, std::move(layouts)); } std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format) { diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.hpp b/core/libs/prx/libSceAudiodec/src/Codecs.hpp index ebf8434eb2..6d43c67e6b 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.hpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.hpp @@ -57,7 +57,7 @@ bool ParseMp3Header(const std::uint8_t* data, std::size_t size, Mp3Header& heade std::uint32_t AacSampleRate(std::uint32_t index); bool ValidAt9Config(const std::uint8_t (&config)[4]); std::unique_ptr CreateMp3(std::int32_t wordSize); -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels); +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency); std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format); } diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index bdc296e7eb..0acb2eeeb0 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -11,11 +11,17 @@ if(BUILD_TESTING) configure_windows_unwind(guest_avplayer_tests) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer) add_test(NAME guest_avplayer COMMAND guest_avplayer_tests) + add_test(NAME guest_avplayer_callback_close COMMAND guest_avplayer_tests --callback-close) set_tests_properties(guest_avplayer PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer TIMEOUT 60) + set_tests_properties(guest_avplayer_callback_close PROPERTIES + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer + TIMEOUT 20 RUN_SERIAL TRUE) if(WIN32) set_tests_properties(guest_avplayer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_avplayer_callback_close PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index fc78bf88b5..59ce8e72a8 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -112,7 +112,7 @@ int APS5_VABI sceAvPlayerInitEx(const AvPlayerInitDataEx* init_ex, AvPlayerInter init.auto_start = init_ex->auto_start; } init.default_language = init_ex->default_language; - *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers)); + *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers), init_ex->this_size >= Ps5InitDataExSize); return SCE_OK; } diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 740f552d2e..97d3be2383 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -32,6 +32,7 @@ constexpr std::int32_t EventWarningId = 0x20; constexpr std::uint32_t StreamTypeVideo = 1; constexpr std::uint32_t StreamTypeAudio = 0; +constexpr std::uint32_t Ps5StreamTypeAudio = 2; constexpr std::uint32_t StreamTypeTimedText = 2; constexpr std::uint32_t SourceTypeUnknown = 0; @@ -86,7 +87,7 @@ std::uint32_t DetectSourceType(std::string_view path); class Player final : public AvPlayerInternal, private ISourceEvents { public: - Player(const AvPlayerInitData& init, std::uint32_t bufferCount); + Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout = false); ~Player() override; Player(const Player&) = delete; Player& operator=(const Player&) = delete; @@ -126,7 +127,7 @@ private: void OnWarning(std::int32_t code) override; void OnError() override; void queue(const Event& event); - void controllerLoop(); + void controllerLoop(const bool& closed); void deliver(const Event& event); void autoStart(); void checkEndOfFile(); @@ -138,6 +139,7 @@ private: AvPlayerMemAllocator memory{}; AvPlayerEventReplacement callback{}; bool autoStartEnabled = false; + bool ps5Layout = false; char language[4]{}; std::uint32_t videoBuffers = 2; @@ -154,6 +156,7 @@ private: TimedWait::Condition eventCondition; std::deque events; bool quit = false; + std::shared_ptr controllerClosed = std::make_shared(false); std::thread controller; }; diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index 6f418b8421..cf28da31f7 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -61,8 +61,8 @@ std::uint32_t DetectSourceType(std::string_view path) { return SourceTypeUnknown; } -Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount) - : memory(WithGuestHeapFallback(init.memory_replacement)), callback(init.event_replacement), videoBuffers(bufferCount) { +Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout) + : memory(WithGuestHeapFallback(init.memory_replacement)), callback(init.event_replacement), ps5Layout(ps5Layout), videoBuffers(bufferCount) { if (init.file_replacement.open && init.file_replacement.close && init.file_replacement.read_offset && init.file_replacement.size) { file = init.file_replacement; } @@ -70,7 +70,7 @@ Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount) if (init.default_language) { for (std::size_t index = 0; index < 3 && init.default_language[index] != '\0'; ++index) language[index] = init.default_language[index]; } - controller = std::thread([this] { controllerLoop(); }); + controller = std::thread([this, closed = controllerClosed] { controllerLoop(*closed); }); } Player::~Player() { @@ -79,7 +79,12 @@ Player::~Player() { quit = true; } eventCondition.NotifyAll(); - controller.join(); + if (controller.get_id() == std::this_thread::get_id()) { + *controllerClosed = true; + controller.detach(); + } else { + controller.join(); + } std::lock_guard lock(mutex); source.reset(); } @@ -100,7 +105,7 @@ void Player::OnError() { queue({0, 0, false, true}); } -void Player::controllerLoop() { +void Player::controllerLoop(const bool& closed) { for (;;) { Event event{}; bool pending = false; @@ -119,6 +124,7 @@ void Player::controllerLoop() { } else { checkEndOfFile(); } + if (closed) return; } } @@ -163,7 +169,7 @@ void Player::autoStart() { return; } if (info.type == StreamTypeVideo && (video == -1 || SameLanguage(language, info.details.video.language_code))) video = index; - if (info.type == StreamTypeAudio && (audio == -1 || SameLanguage(language, info.details.audio.language_code))) audio = index; + if (info.type == (ps5Layout ? Ps5StreamTypeAudio : StreamTypeAudio) && (audio == -1 || SameLanguage(language, info.details.audio.language_code))) audio = index; } if (video != -1) EnableStream(static_cast(video)); if (audio != -1) EnableStream(static_cast(audio)); @@ -227,6 +233,7 @@ int Player::StreamCount() { int Player::GetStreamInfo(std::uint32_t index, AvPlayerStreamInfo& info) { std::lock_guard lock(mutex); if (!source || !source->GetStreamInfo(index, info)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; + if (ps5Layout && info.type == StreamTypeAudio) info.type = Ps5StreamTypeAudio; return SCE_OK; } @@ -234,6 +241,7 @@ int Player::GetStreamInfoEx(std::uint32_t index, AvPlayerStreamInfoEx& info) { std::lock_guard lock(mutex); AvPlayerStreamInfoEx described{}; if (!source || !source->GetStreamInfoEx(index, described)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; + if (ps5Layout && described.type == StreamTypeAudio) described.type = Ps5StreamTypeAudio; described.this_size = info.this_size; info = described; return SCE_OK; diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 7778568ddc..b9c1e162f3 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -63,7 +63,7 @@ std::uint64_t FrameMillis(const AVFrame& frame, AVRational timeBase) { bool IsStreamSupported(const AVStream& stream) { const auto codec = stream.codecpar->codec_id; - return codec == AV_CODEC_ID_H264 || codec == AV_CODEC_ID_HEVC || codec == AV_CODEC_ID_AAC; + return codec == AV_CODEC_ID_H264 || codec == AV_CODEC_ID_HEVC || codec == AV_CODEC_ID_AAC || codec == AV_CODEC_ID_VP9 || codec == AV_CODEC_ID_OPUS; } float DisplayAspect(int width, int height, AVRational sampleAspect) { @@ -343,6 +343,7 @@ public: demuxEpoch = 1; minEpoch = 1; clockEpoch = 0; + loopEpochs.clear(); audioDriving = false; presented = false; lastPresented = 0; @@ -579,6 +580,8 @@ private: picture.luma_bit_depth = 8; picture.chroma_bit_depth = 8; picture.video_full_range_flag = parameters->color_range == AVCOL_RANGE_JPEG; + const auto rate = av_guess_frame_rate(format, const_cast(&stream), nullptr); + picture.frame_rate = rate.num > 0 && rate.den > 0 ? av_q2d(rate) : 0.0; } else { auto& sound = info.details.audio; sound.channel_count = basic.details.audio.channel_count; @@ -716,6 +719,12 @@ private: decoder.current = std::move(decoder.frames.front()); decoder.frames.pop_front(); info = decoder.current->info; + bool looped = false; + while (!loopEpochs.empty() && loopEpochs.front() <= decoder.current->epoch) { + looped = looped || loopEpochs.front() == decoder.current->epoch; + loopEpochs.pop_front(); + } + if (looped) events.OnWarning(SCE_AVPLAYER_ERROR_WAR_LOOPING_BACK); } void dropLateAudio() { @@ -756,6 +765,7 @@ private: presentationClock.Rebase(static_cast(requested->target)); ++demuxEpoch; minEpoch = demuxEpoch; + loopEpochs.clear(); for (auto* decoder : {&video, &audio}) { if (decoder->stream < 0) continue; for (auto& item : decoder->packets) av_packet_free(&item.packet); @@ -788,10 +798,10 @@ private: { std::lock_guard lock(mutex); ++demuxEpoch; + loopEpochs.push_back(demuxEpoch); pushAll(PacketKind::Drain, demuxEpoch, 0); } condition.notify_all(); - events.OnWarning(SCE_AVPLAYER_ERROR_WAR_LOOPING_BACK); continue; } { @@ -980,6 +990,8 @@ private: info.details.video.luma_bit_depth = 8; info.details.video.chroma_bit_depth = 8; info.details.video.video_full_range_flag = frame.color_range == AVCOL_RANGE_JPEG; + const auto rate = av_guess_frame_rate(format, format->streams[index], nullptr); + info.details.video.frame_rate = rate.num > 0 && rate.den > 0 ? av_q2d(rate) : 0.0; publish(decoder, output); return true; } @@ -1071,6 +1083,7 @@ private: std::uint64_t demuxEpoch = 0; std::uint64_t minEpoch = 0; std::uint64_t clockEpoch = 0; + std::deque loopEpochs; bool audioDriving = false; bool presented = false; std::uint64_t lastPresented = 0; diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index b19768c25a..7113ee245a 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,9 +10,11 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); 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_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); } @@ -21,7 +22,7 @@ extern "C" { int APS5_VABI scePlayerReviewDialogInitialize(void) { int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("scePlayerReviewDialogInitialize: already initialized"); + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } @@ -29,8 +30,9 @@ int APS5_VABI scePlayerReviewDialogTerminate(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("scePlayerReviewDialogTerminate: not initialized or still running"); - return 0; + if (g_status.compare_exchange_strong(expected, 0)) return 0; + if (g_status.load() == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI scePlayerReviewDialogOpen(const void* param) { diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 4c80330e30..098239068f 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -8,6 +8,10 @@ namespace { std::atomic g_initialized{false}; + +void RequireInitialized(const char* function) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); +} } extern "C" { @@ -19,14 +23,15 @@ int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_para return 0; } -int APS5_VABI sceContentDeleteByPath(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentDeleteByPath(const char* contentPath) { + if (contentPath == nullptr) APS5_INVALID_ARG_EX; + RequireInitialized(__func__); + throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } -int APS5_VABI sceContentDeleteById(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentDeleteById(std::int64_t contentId) { + RequireInitialized(__func__); + 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 sceContentDeleteTerminate(void) { diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 1ea0a503ae..4dd9a470cb 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -13,13 +13,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - 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 libSceContentDelete libc) - configure_windows_unwind(guest_content_unimplemented_tests) - add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) + add_test_executable(guest_content_delete_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentDelete.cpp) + target_include_directories(guest_content_delete_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_content_delete_tests PRIVATE libSceContentDelete libc) + configure_windows_unwind(guest_content_delete_tests) + add_test(NAME guest_content_delete COMMAND guest_content_delete_tests) if(WIN32) - set_tests_properties(guest_content_unimplemented PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_content_delete PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 29c849a7e1..9b9e92908f 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -8,6 +8,7 @@ namespace { constexpr int kErrInvalidAddress = static_cast(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast(0x80BC0010u); +constexpr std::uint32_t kIgnoredStateBits = 0x4000000u; char16_t UsCharacter(std::uint16_t keycode) { static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; @@ -17,6 +18,7 @@ char16_t UsCharacter(std::uint16_t keycode) { if (keycode == 0x27) return u'0'; if (keycode == 0x2C) return u' '; if (keycode >= 0x2D && keycode <= 0x38) return kPunctuation[keycode - 0x2D]; + if (keycode == 0x64) return u'\\'; if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; return 0; } @@ -43,7 +45,7 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { if (character == nullptr) return kErrInvalidAddress; - if (state != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); + if ((state & ~kIgnoredStateBits) != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); *character = UsCharacter(keycode); return 0; diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index 6aa8ddda2a..883b7fc154 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -8,6 +8,7 @@ if(BUILD_TESTING) target_include_directories(fiber_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(fiber_tests PRIVATE libSceFiber libc) add_test(NAME fiber COMMAND fiber_tests) + add_asan_test_options(fiber detect_stack_use_after_return=0) if(WIN32) set_tests_properties(fiber PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 7c3a53dc03..ea130e26f6 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -35,6 +35,7 @@ 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; +static constexpr std::size_t FIBER_GUEST_STACK_MIN = 64u << 10; using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); @@ -331,6 +332,7 @@ static std::uint64_t* SizedWindow(const Fiber* fiber) { #ifdef _WIN32 static StackBounds FiberBounds(const Fiber* fiber) { + if (!fiber->hostStack) return {fiber->context + fiber->contextSize, fiber->context, fiber->context}; return {fiber->hostStack + fiber->hostStackBytes, fiber->hostStack + HostStackGuardBytes(), fiber->hostStack}; } @@ -421,7 +423,7 @@ 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); + if (size_context < FIBER_GUEST_STACK_MIN) AllocateHostStack(fiber); fiber->contextSizeCheck = g_contextSizeCheck.load(std::memory_order_relaxed); if (fiber->contextSizeCheck) { auto* words = SizedWindow(fiber); diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 458e3e5fd7..8fa51ea749 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -5,6 +5,7 @@ if(BUILD_TESTING) add_test_executable(guest_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestFont.cpp) target_include_directories(guest_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_tests) add_test(NAME guest_font COMMAND guest_font_tests) add_test_executable(guest_system_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFont.cpp) target_include_directories(guest_system_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp index f5b1e9b6a9..eb1a548bb4 100644 --- a/core/libs/prx/libSceFont/include/FontTypes.hpp +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -375,6 +375,15 @@ struct FontWritingLetterStep { } Components; }; +union FontTextCodes { + struct { + void* order; + std::uint32_t code; + std::uint32_t reserved; + } text; + void* systemUse[8]; +}; + struct FontWritingExtent { float top; float bottom; @@ -441,6 +450,7 @@ static_assert(sizeof(FontWriting) == 0x100); static_assert(sizeof(FontWritingProfile) == sizeof(std::uint32_t)); static_assert(sizeof(FontWritingStep) == 0x48); static_assert(sizeof(FontWritingLetterStep) == 0x40); +static_assert(sizeof(FontTextCodes) == 0x40); static_assert(sizeof(FontWritingMetrics) == 0x18); static_assert(sizeof(FontWritingLineStep) == 0x40); static_assert(offsetof(FontWritingLineStep, writingOrderer) == 0x18); diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index c34ed9a2dd..afc8a452f7 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -208,6 +208,20 @@ void AccumulateWritingMetrics(FontWritingData& writing) { writing.metrics.Extent.bottom = std::max(previous.Extent.bottom, bottom); } +constexpr std::size_t TEXT_CODES_CURRENT = 5; +constexpr std::size_t TEXT_CODES_START = 6; +constexpr std::size_t TEXT_CODES_TERM = 7; + +void StepTextCodes(FontTextCodes* textCodes, const FontTextCharacter* character) { + textCodes->text.order = character->textOrder; + textCodes->text.code = character->characterCode; + textCodes->systemUse[TEXT_CODES_CURRENT] = const_cast(character); +} + +const FontTextCharacter* TextCodesCharacter(const FontTextCodes* textCodes, std::size_t slot) { + return static_cast(textCodes->systemUse[slot]); +} + } #pragma GCC visibility push(default) @@ -551,6 +565,39 @@ FontTextCharacter* APS5_VABI sceFontCharacterRefersTextNext(const FontTextCharac return nullptr; } +FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(const FontTextCharacter* textCharacter, const FontTextCharacter* termCharacter, FontTextCodes* textCodes) { + if (!textCodes) return nullptr; + if (!textCharacter) throw std::runtime_error("libSceFont: sceFontCharactersRefersTextCodes without a text character"); + if (textCharacter->clusterIndex != 0 || textCharacter->synthetic != 0) return nullptr; + std::memset(textCodes, 0, sizeof(FontTextCodes)); + StepTextCodes(textCodes, textCharacter); + textCodes->systemUse[TEXT_CODES_START] = const_cast(textCharacter); + textCodes->systemUse[TEXT_CODES_TERM] = const_cast(termCharacter); + return textCodes; +} + +FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes* textCodesStep) { + if (!textCodesStep) return nullptr; + const FontTextCharacter* current = TextCodesCharacter(textCodesStep, TEXT_CODES_CURRENT); + if (!current || current == TextCodesCharacter(textCodesStep, TEXT_CODES_TERM)) return nullptr; + const FontTextCharacter* next = current->next; + while (next && next->synthetic != 0) next = next->next; + if (!next) return nullptr; + StepTextCodes(textCodesStep, next); + return textCodesStep; +} + +FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes* textCodesStep) { + if (!textCodesStep) return nullptr; + const FontTextCharacter* current = TextCodesCharacter(textCodesStep, TEXT_CODES_CURRENT); + if (!current || current == TextCodesCharacter(textCodesStep, TEXT_CODES_START)) return nullptr; + const FontTextCharacter* previous = current->prev; + while (previous && previous->synthetic != 0) previous = previous->prev; + if (!previous) return nullptr; + StepTextCodes(textCodesStep, previous); + return textCodesStep; +} + int APS5_VABI sceFontWritingInit(FontWriting* fontWriting, FontString fontString, const FontTextCharacter* fontCharacter) { if (!fontWriting || !fontString || !fontCharacter) return SCE_FONT_ERROR_INVALID_PARAMETER; auto* data = GetStringData(fontString); diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index b3c2e79b7c..aa341a63b5 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -7,11 +7,6 @@ extern "C" { -int APS5_VABI sceFontCharactersRefersTextCodes() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontControl() { NotImplemented_nid_no_patch(__func__); return 0; @@ -358,16 +353,6 @@ int APS5_VABI sceFontSupportGlyphs() { return 0; } -int APS5_VABI sceFontTextCodesStepBack() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextCodesStepNext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontWordsFindWordCharacters() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 4c0d0bb575..7acb4eecc2 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -44,7 +44,7 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (entitlement_label == nullptr) { - NotImplemented_nid_no_patch(__func__); + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 9f1c4cb8f9..e7361d9b35 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(*c); - if (std::isalnum(value) || value == '-' || value == '_' || value == '.' || value == '~') { + if ((value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') || (value >= '0' && value <= '9') + || value == '-' || value == '_' || value == '.' || value == '~') { escaped.push_back(*c); } else { escaped.push_back('%'); diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index d686445211..8d2f2748ee 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -346,7 +346,7 @@ int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* int APS5_VABI sceImeKeyboardClose(int32_t user_id) { std::lock_guard lock(g_keyboardMutex); - if (g_openKeyboards.erase(user_id) == 0) throw std::logic_error("sceImeKeyboardClose: keyboard not open for user " + std::to_string(user_id)); + if (g_openKeyboards.erase(user_id) == 0) return ErrorNotOpened; return 0; } @@ -369,7 +369,7 @@ int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArr int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { if (!param) APS5_INVALID_ARG_EX; std::lock_guard lock(g_keyboardMutex); - if (!g_openKeyboards.insert(user_id).second) throw std::logic_error("sceImeKeyboardOpen: keyboard already open for user " + std::to_string(user_id)); + if (!g_openKeyboards.insert(user_id).second) return ErrorBusy; return 0; } diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 8356a4cb52..7ca113639e 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #ifdef _WIN32 #include #else @@ -141,14 +140,4 @@ unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds); 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/libSceLoginService/CMakeLists.txt b/core/libs/prx/libSceLoginService/CMakeLists.txt new file mode 100644 index 0000000000..5993f09b49 --- /dev/null +++ b/core/libs/prx/libSceLoginService/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceLoginService SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLoginService PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLoginService) + +if(BUILD_TESTING) + add_test_executable(guest_login_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestLoginService.cpp) + target_include_directories(guest_login_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_login_service_tests PRIVATE libSceLoginService libc) + configure_windows_unwind(guest_login_service_tests) + add_test(NAME guest_login_service COMMAND guest_login_service_tests) + if(WIN32) + set_tests_properties(guest_login_service PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLoginService/Export.cpp b/core/libs/prx/libSceLoginService/Export.cpp new file mode 100644 index 0000000000..43a1b1dcdf --- /dev/null +++ b/core/libs/prx/libSceLoginService/Export.cpp @@ -0,0 +1,21 @@ +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceLoginServiceInitialize(void) { + return 0; +} + +int APS5_VABI sceLoginServiceTerminate(void) { + return 0; +} + +int APS5_VABI sceLoginServiceRequestDevices(void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 0fd4352a9f..7278fa33eb 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,5 +1,4 @@ #include "prx/libSceMouse/include/MouseState.hpp" -#include "prx/libc/include/General.hpp" extern "C" { @@ -23,9 +22,8 @@ void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } -int APS5_VABI sceMouseResetAbsolutePosition() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y) { + return Mouse::ResetAbsolutePosition(handle, x, y); } bool MouseIsOpen_nid_no_patch() { diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 5abc3c3e6a..390cf33968 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -21,6 +21,7 @@ int Initialize(); int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); int Close(std::int32_t handle); int Read(std::int32_t handle, MouseData* data, std::int32_t num); +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y); void Publish(const MouseInputEvent& event); } diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index c139fff6a2..d73ccb929d 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -82,6 +82,13 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t, std::uint16_t) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + return MOUSE_OK; +} + void Publish(const MouseInputEvent& event) { std::lock_guard lock(mouseMutex); if (event.connectionChange) { diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index 2793ec0184..ffada31501 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -11,21 +11,26 @@ int APS5_VABI sceMouseInit(); int APS5_VABI sceMouseOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceMouseClose(std::int32_t); int APS5_VABI sceMouseRead(std::int32_t, MouseData*, std::int32_t); +int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t, std::uint16_t, std::uint16_t); } static void Require(bool value) { if (!value) std::abort(); } int main() { MouseData data[64]{}; + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseInit() == MOUSE_OK); Require(sceMouseInit() == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseOpen(1, 0, 1, nullptr) == MOUSE_ERROR_INVALID_ARG); MouseOpenParam unsupported{}; unsupported.behaviorFlag = 2; Require(sceMouseOpen(1, 0, 0, &unsupported) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); + Require(sceMouseResetAbsolutePosition(42, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); Require(sceMouseRead(42, data, 1) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseRead(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseRead(MOUSE_HANDLE, data, 65) == MOUSE_ERROR_INVALID_ARG); @@ -99,10 +104,13 @@ int main() { Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); MouseOpenParam merged{}; merged.behaviorFlag = MOUSE_OPEN_PARAM_MERGED; Require(sceMouseOpen(1, 0, 0, &merged) == MOUSE_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].buttons == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); } diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index edb76df0f7..339bf0049a 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,6 +10,7 @@ std::atomic g_status{0}; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_OK = 0; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); 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_NOT_RUNNING = static_cast(0x80B8000Bu); @@ -23,41 +23,38 @@ extern "C" { int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; int APS5_VABI sceMsgDialogClose(void) { - int expected = COMMON_DIALOG_STATUS_RUNNING; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; - return 0; + int expected = COMMON_DIALOG_STATUS_RUNNING; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; + return 0; } int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - *result = MsgDialogResult{}; - result->result = COMMON_DIALOG_RESULT_OK; - result->button_id = BUTTON_ID_OK; - return 0; + const int status = g_status.load(); + if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + *result = MsgDialogResult{}; + result->result = COMMON_DIALOG_RESULT_OK; + result->button_id = BUTTON_ID_OK; + return 0; } int APS5_VABI sceMsgDialogInitialize(void) { int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceMsgDialogInitialize: already initialized"); + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } int APS5_VABI sceMsgDialogOpen(const void* param) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; + const int status = g_status.load(); + if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceMsgDialogTerminate(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("sceMsgDialogTerminate: not initialized or still running"); + if (g_status.exchange(0) == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } @@ -66,21 +63,21 @@ int APS5_VABI sceMsgDialogUpdateStatus(void) { } int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { - (void)target; - (void)delta; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)delta; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { - (void)target; - (void)msg; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)msg; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { - (void)target; - (void)rate; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)rate; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogGetStatus(void) { diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 98776d4b5e..4a1c9fdb5e 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -15,6 +15,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_refused_connect_tests ${LIBS_INCLUDE_DIR}/tests/GuestRefusedConnect.cpp) + target_include_directories(guest_refused_connect_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_refused_connect_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_refused_connect_tests) + add_test(NAME guest_refused_connect COMMAND guest_refused_connect_tests) + set_tests_properties(guest_refused_connect PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_refused_connect PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(guest_socket_host_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketHostWrite.cpp) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index ac697a4120..8fd8578762 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -87,6 +87,7 @@ constexpr int NET_SO_ERROR = 0x1007; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; +constexpr int NET_SO_ONESBCAST = 0x10000; constexpr int NET_SO_SNDBUF = 0x1001; constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_SO_SCE_SNDTIMEO = 0x1105; @@ -246,11 +247,13 @@ struct Sock { bool bound = false; bool listening = false; bool aborted = false; + bool read_shut = false; int rcv_timeout_us = 0; int snd_timeout_us = 0; int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; + int onesbcast = 0; }; #ifdef _WIN32 @@ -491,6 +494,59 @@ int native_message_flags(int flags) { return native; } +#ifdef _WIN32 +bool has_local_address(NativeSocket native) { + sockaddr_storage local{}; + NativeLength local_length = sizeof(local); + return ::getsockname(native, reinterpret_cast(&local), &local_length) == 0 || WSAGetLastError() != WSAEINVAL; +} + +void note_local_address(int s, const Sock& socket) { + if (socket.type == NET_SOCK_STREAM || socket.bound || !has_local_address(socket.native->value)) return; + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it != g_socks.end() && it->second.native == socket.native) it->second.bound = true; + g_cv.NotifyAll(); +} + +constexpr int RECEIVE_NOTHING = -1; + +int wait_for_datagram(const Sock& socket, std::chrono::steady_clock::time_point deadline) { + if (deadline == std::chrono::steady_clock::time_point::max()) return 0; + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) return NET_EAGAIN; + const auto remaining = std::chrono::ceil(deadline - now).count(); + WSAPOLLFD descriptor{}; + descriptor.fd = socket.native->value; + descriptor.events = POLLRDNORM; + return WSAPoll(&descriptor, 1, static_cast(std::min(remaining, INT_MAX))) == 0 ? NET_EAGAIN : 0; +} + +int wait_for_local_address(int s, const Sock& socket, bool empty) { + if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; + const bool named = has_local_address(socket.native->value); + if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; + if (!named && socket.nonblock) return NET_EAGAIN; + const auto deadline = socket.rcv_timeout_us > 0 + ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) + : std::chrono::steady_clock::time_point::max(); + std::unique_lock lk(g_mutex); + for (bool waited = false;; waited = true) { + const auto it = g_socks.find(s); + if (it == g_socks.end() || it->second.native != socket.native || it->second.aborted) return 0; + if (named || has_local_address(socket.native->value)) { + it->second.bound = true; + lk.unlock(); + return waited ? wait_for_datagram(socket, deadline) : 0; + } + if (it->second.read_shut) return RECEIVE_NOTHING; + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) return NET_EAGAIN; + g_cv.WaitUntil(lk, std::min(deadline, now + std::chrono::milliseconds(10))); + } +} +#endif + } // namespace extern "C" { @@ -503,7 +559,6 @@ int* APS5_VABI sceNetErrnoLoc(void) { } int APS5_VABI sceNetShowNetstat(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -766,6 +821,7 @@ int APS5_VABI sceNetBind_nid_postfix(int s, const void* addr, uint32_t addrlen) std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) it->second.bound = true; + g_cv.NotifyAll(); return 0; } @@ -859,6 +915,10 @@ static int disconnect_datagram(const Sock& socket) { return 0; } +#ifdef _WIN32 +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address); +#endif + int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { Sock socket; { @@ -875,8 +935,16 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { sockaddr_storage native{}; NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); +#ifdef _WIN32 + if (socket.type == NET_SOCK_STREAM) RefuseClosedLoopbackAtOnce_nid_no_patch(socket.native->value, reinterpret_cast(&native)); +#endif if (native.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) return 0; + if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) { +#ifdef _WIN32 + note_local_address(s, socket); +#endif + return 0; + } #ifdef _WIN32 if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); #endif @@ -894,6 +962,9 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef _WIN32 + if (const int wait = wait_for_local_address(s, socket, len == 0)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); +#endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); const int native_flags = native_message_flags(flags); @@ -916,6 +987,13 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef _WIN32 + if (const int wait = wait_for_local_address(s, socket, len == 0)) { + if (wait != RECEIVE_NOTHING) return fail(wait); + if (from) *fromlen = 0; + return 0; + } +#endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; @@ -969,7 +1047,11 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), reinterpret_cast(&destination), destination_length); } - return result >= 0 ? result : fail(native_error()); + if (result < 0) return fail(native_error()); +#ifdef _WIN32 + note_local_address(s, socket); +#endif + return result; } int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { @@ -1002,6 +1084,15 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { socket = it->second; } const bool datagram = socket.type != NET_SOCK_STREAM; +#ifdef _WIN32 + if (const int wait = wait_for_local_address(s, socket, total == 0)) { + if (wait != RECEIVE_NOTHING) return fail(wait); + msg->flags = 0; + if (msg->name) msg->name_length = 0; + msg->control_length = 0; + return 0; + } +#endif std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); @@ -1045,7 +1136,14 @@ int APS5_VABI sceNetShutdown(int s, int how) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - return ::shutdown(socket.native->value, how) == 0 ? 0 : fail(native_error()); + const int error = ::shutdown(socket.native->value, how) == 0 ? 0 : native_error(); + if (how != 1) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it != g_socks.end() && it->second.native == socket.native) it->second.read_shut = true; + g_cv.NotifyAll(); + } + return error == 0 ? 0 : fail(error); } int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { @@ -1057,6 +1155,10 @@ 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); + if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { + const int error = set_kernel_timeout(s, optname, optval, optlen); + return error == 0 ? 0 : fail(error); + } optname = timeout_option(optname); if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); int value = 0; @@ -1077,6 +1179,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) return fail(native_error()); + } else if (optname == NET_SO_ONESBCAST) { } else { return fail(NET_EOPNOTSUPP); } @@ -1088,6 +1191,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; + if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; } return 0; } @@ -1100,15 +1204,22 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - if (optval == nullptr || optlen == nullptr || *optlen < sizeof(int)) { - return fail(NET_EINVAL); + if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); + if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { + const int microseconds = optname == NET_SO_RCVTIMEO ? socket.rcv_timeout_us : socket.snd_timeout_us; + const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; + *optlen = std::min(*optlen, static_cast(sizeof(timeout))); + std::memcpy(optval, &timeout, *optlen); + return 0; } + if (*optlen < sizeof(int)) return fail(NET_EINVAL); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_ONESBCAST: value = socket.onesbcast; break; case NET_SO_ERROR: { int pending = 0; NativeLength size = sizeof(pending); @@ -1607,7 +1718,6 @@ int APS5_VABI sceNetResolverAbort(int rid, int flags) { } int APS5_VABI sceNetShowIfconfig() { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 27bdd35a7c..2541758590 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -43,12 +43,15 @@ static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; static constexpr int NET_CTL_INFO_LINK = 4; static constexpr int NET_CTL_INFO_IP_CONFIG = 11; +static constexpr int NET_CTL_INFO_DHCP_HOSTNAME = 12; +static constexpr int NET_CTL_INFO_PPPOE_AUTH_NAME = 13; static constexpr int NET_CTL_INFO_IP_ADDRESS = 14; static constexpr int NET_CTL_INFO_NETMASK = 15; static constexpr int NET_CTL_INFO_DEFAULT_ROUTE = 16; static constexpr int NET_CTL_INFO_PRIMARY_DNS = 17; static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; +static constexpr int NET_CTL_INFO_HTTP_PROXY_SERVER = 20; static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; @@ -349,13 +352,17 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + case NET_CTL_INFO_HTTP_PROXY_PORT: WriteInfoWord(info, 0); break; + case NET_CTL_INFO_DHCP_HOSTNAME: + case NET_CTL_INFO_PPPOE_AUTH_NAME: + case NET_CTL_INFO_HTTP_PROXY_SERVER: std::memset(info->opaque, 0, sizeof(info->opaque)); break; default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); } return 0; } int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { - (void)nat_info; + if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR; return SCE_NET_CTL_ERROR_NOT_CONNECTED; } diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index ca10bd5243..0fd1c2376a 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 19e7cd543d..6f6bb45435 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,8 +9,9 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); -static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A0230); -static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A0261); +static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A8201); +static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A8202); +static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast(0x804A8305); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 5f163c25ee..d3e586a4d9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -136,7 +136,9 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { filter.location = 1; filter.type = custom.type; filter.channel_mask = custom.channel_mask; - if (custom.type == 1) { + if (custom.type == 0) { + filter = {}; + } else if (custom.type == 1) { filter.frequency = custom.param.fcq.frequency; filter.q = custom.param.fcq.q; filter.level = custom.param.fcq.level; diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index f8ab93148e..f7571f6d8e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -1,26 +1,61 @@ +#include +#include #include #include +#include +#include #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" static constexpr float DEFAULT_SOUND_SPEED = 343.0f; +namespace { + +using Vector = std::array; + +Vector ReadVector(const Ngs2GeomVector& value) { + if (!std::isfinite(value.x) || !std::isfinite(value.y) || !std::isfinite(value.z)) APS5_INVALID_ARG_EX; + return {value.x, value.y, value.z}; +} + +double Dot(const Vector& left, const Vector& right) { + return left[0] * right[0] + left[1] * right[1] + left[2] * right[2]; +} + +Vector Unit(Vector value) { + const double length = std::hypot(value[0], value[1], value[2]); + if (!(length > 0.0)) APS5_INVALID_ARG_EX; + for (auto& component : value) component /= length; + return value; +} + +} + 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 (flags > 1) throw std::runtime_error("NGS2: unknown listener coordinate flags"); if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; + const Vector position = ReadVector(param->position); + ReadVector(param->velocity); + const Vector front = Unit(ReadVector(param->orient_front)); + Vector up = Unit(ReadVector(param->orient_up)); + Vector right = Unit({up[1] * front[2] - up[2] * front[1], up[2] * front[0] - up[0] * front[2], up[0] * front[1] - up[1] * front[0]}); + up = {front[1] * right[2] - front[2] * right[1], front[2] * right[0] - front[0] * right[2], front[0] * right[1] - front[1] * right[0]}; + if (flags == 1) for (auto& component : right) component = -component; + const std::array axes{right, up, front}; Ngs2GeomListenerWork work{}; - for (std::size_t i = 0; i < 4; i++) work.matrix[i][i] = 1.0f; + for (std::size_t row = 0; row < axes.size(); row++) { + for (std::size_t column = 0; column < 3; column++) work.matrix[row][column] = static_cast(axes[row][column]); + const double translation = -Dot(axes[row], position); + if (std::abs(translation) > std::numeric_limits::max()) APS5_INVALID_ARG_EX; + work.matrix[row][3] = static_cast(translation); + } + work.matrix[3][3] = 1.0f; work.velocity = param->velocity; work.sound_speed = param->sound_speed; + work.coordinate = flags; *outWork = work; return SCE_NGS2_OK; } @@ -53,3 +88,19 @@ int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { out_source_param->matrix_format = 2; return SCE_NGS2_OK; } + +int Ngs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* out_attrib, std::uint32_t flags) { + if (listener == nullptr || source == nullptr) APS5_INVALID_ARG_EX; + if (out_attrib == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + (void)flags; + std::memset(out_attrib, 0, sizeof(*out_attrib)); + out_attrib->pitch_ratio = 1.0f; + out_attrib->a3d_attrib.position = source->position; + out_attrib->a3d_attrib.volume = std::max(source->min_level, source->max_level); + const std::uint32_t channels = std::min(source->matrix_format == 0u ? 2u : source->matrix_format, 8u); + const float level = source->max_level > 0.0f ? source->max_level : 1.0f; + for (std::uint32_t channel = 0; channel < channels; channel++) { + out_attrib->level[static_cast(channel) * 8 + channel] = level; + } + return SCE_NGS2_OK; +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index b219666620..e71d56b87c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -209,11 +209,13 @@ void Ngs2ClearReverb(Ngs2Voice& voice); bool Ngs2ProcessReverb(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2ProcessLegacyUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); +void Ngs2RenderSilence(const Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest); 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 Ngs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* outAttrib, 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 a9df1495ce..8261f9ad42 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -200,11 +200,11 @@ int APS5_VABI sceNgs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo* b } int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id, uintptr_t* handle) { - if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; std::lock_guard lock(Ngs2Mutex()); auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; - if (voice_id >= rack->voices.size()) APS5_INVALID_ARG_EX; + if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (voice_id >= rack->voices.size()) return SCE_NGS2_ERROR_INVALID_VOICE_INDEX; *handle = reinterpret_cast(&rack->voices[voice_id]); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 5ed62eff86..a3ec266e0a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -95,16 +95,19 @@ static void Advance(Ngs2Voice& voice, std::uint64_t frames) { if (block.streaming) { while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin()); } - if (block.cursor == block.info.num_samples || (block.streaming && !voice.acceptsBlocks && block.cursor == block.availableFrames)) Ngs2FinishBlock(voice); + if (block.cursor == block.info.num_samples || (block.streaming && !voice.acceptsBlocks && block.cursor == block.availableFrames)) { + if (!Ngs2FinishBlock(voice)) return; + } } } static void ConsumeSamples(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { + const auto revision = voice.waveformRevision; const auto step = static_cast(std::llround(voice.sampleRate * static_cast(voice.pitch) / systemRate * PHASE_ONE)); std::uint32_t written = 0; float current[NGS2_MAX_CHANNELS]; float next[NGS2_MAX_CHANNELS]; - for (; written < grain && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty(); written++) { + for (; written < grain && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty() && voice.waveformRevision == revision; written++) { ReadFrames(voice, current, next); const auto fraction = static_cast(static_cast(voice.phase) / PHASE_ONE); for (std::uint32_t channel = 0; channel < voice.channels; channel++) { @@ -146,8 +149,14 @@ static void ApplyFilter(Ngs2Voice& voice, Ngs2Filter& filter, std::uint32_t grai } static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { + const auto channels = voice.channels; if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2ConsumeAtrac9(voice, grain, systemRate); else ConsumeSamples(voice, grain, systemRate); + if (voice.channels != channels) { + voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); + voice.hasSamples = false; + return; + } for (auto& filter : voice.filters) { if (!filter.enabled || (!voice.hasSamples && !HasHistory(filter))) continue; ApplyFilter(voice, filter, grain); @@ -289,7 +298,7 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector OutputBytes(const Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) { const auto grain = system.option.num_grain_samples; std::vector outputBytes(numBufferInfo); for (std::uint32_t i = 0; i < numBufferInfo; i++) { @@ -301,6 +310,19 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo outputBytes[i] = static_cast(grain) * output.num_channels * SampleBytes(output.waveform_type); if (output.buffer_size < outputBytes[i]) throw std::invalid_argument("NGS2: render buffer " + std::to_string(i) + " is too small"); } + return outputBytes; +} + +void Ngs2RenderSilence(const Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) { + const auto outputBytes = OutputBytes(system, bufferInfo, numBufferInfo); + for (std::uint32_t i = 0; i < numBufferInfo; i++) { + if (outputBytes[i] != 0) std::memset(bufferInfo[i].buffer, 0, outputBytes[i]); + } +} + +void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) { + const auto grain = system.option.num_grain_samples; + const auto outputBytes = OutputBytes(system, bufferInfo, numBufferInfo); std::vector voices; for (auto* rack : system.racks) { for (auto& voice : rack->voices) { diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index cf861a6ae8..17e696d525 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -1,13 +1,16 @@ #include +#include #include #include #include #include +#include #include #include #include #include #include +#include #include #include "prx/libc/include/General.hpp" @@ -30,6 +33,34 @@ std::recursive_mutex& Ngs2Mutex() { return *mutex; } +struct SystemLockState { + std::thread::id owner; + std::uint32_t depth = 0; +}; + +static std::mutex& SystemLockMutex() { + static auto* const mutex = new std::mutex(); + return *mutex; +} + +static std::condition_variable& SystemLockReleased() { + static auto* const released = new std::condition_variable(); + return *released; +} + +static std::map& SystemLocks() { + static auto* const locks = new std::map(); + return *locks; +} + +static void ReleaseSystemLock(SystemLockState& state) { + if (state.depth == 0) return; + if (--state.depth == 0) { + state.owner = {}; + SystemLockReleased().notify_all(); + } +} + Ngs2System* Ngs2FindSystem(Ngs2Handle handle) { auto& systems = Systems(); const auto found = std::find(systems.begin(), systems.end(), reinterpret_cast(handle)); @@ -110,6 +141,8 @@ static int CreateSystem(const Ngs2SystemOption& option, const Ngs2ContextBufferI system->uid = nextUid++; Systems().push_back(system); *handle = reinterpret_cast(system); + std::lock_guard lock(SystemLockMutex()); + SystemLocks()[*handle] = {}; return SCE_NGS2_OK; } @@ -151,6 +184,11 @@ int APS5_VABI sceNgs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferInf if (result != SCE_NGS2_OK) return result; } std::erase(Systems(), system); + { + std::lock_guard locks(SystemLockMutex()); + SystemLocks().erase(system_handle); + SystemLockReleased().notify_all(); + } const auto bufferInfo = system->bufferInfo; const auto allocator = system->allocator; std::destroy_at(system); @@ -223,16 +261,25 @@ int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_ } int APS5_VABI sceNgs2SystemLock(uintptr_t system_handle) { - Ngs2Mutex().lock(); - if (Ngs2FindSystem(system_handle) != nullptr) return SCE_NGS2_OK; - Ngs2Mutex().unlock(); - return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + const auto self = std::this_thread::get_id(); + std::unique_lock lock(SystemLockMutex()); + auto& locks = SystemLocks(); + SystemLockReleased().wait(lock, [&] { + const auto found = locks.find(system_handle); + return found == locks.end() || found->second.depth == 0 || found->second.owner == self; + }); + const auto found = locks.find(system_handle); + if (found == locks.end()) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + found->second.owner = self; + found->second.depth++; + return SCE_NGS2_OK; } int APS5_VABI sceNgs2SystemUnlock(uintptr_t system_handle) { - std::lock_guard lock(Ngs2Mutex()); - if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; - Ngs2Mutex().unlock(); + std::lock_guard lock(SystemLockMutex()); + const auto found = SystemLocks().find(system_handle); + if (found == SystemLocks().end()) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + ReleaseSystemLock(found->second); return SCE_NGS2_OK; } @@ -241,6 +288,26 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (buffer_info == nullptr || num_buffer_info == 0) APS5_INVALID_ARG_EX; + SystemLockState* state = nullptr; + { + const auto self = std::this_thread::get_id(); + std::lock_guard locks(SystemLockMutex()); + auto& found = SystemLocks().at(system_handle); + if (found.depth != 0 && found.owner != self) { + Ngs2RenderSilence(*system, buffer_info, num_buffer_info); + return SCE_NGS2_OK; + } + found.owner = self; + found.depth++; + state = &found; + } + struct Release { + SystemLockState* state; + ~Release() { + std::lock_guard locks(SystemLockMutex()); + ReleaseSystemLock(*state); + } + } release{state}; Ngs2RenderSystem(*system, buffer_info, num_buffer_info); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 1541b64394..0cd8a3dd82 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -8,15 +8,6 @@ extern "C" { -int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* out_attrib, uint32_t flags) { - (void)listener; - (void)source; - (void)out_attrib; - (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 1ae32f78af..a19ad7b442 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -514,6 +514,10 @@ int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* p return Ngs2PanGetVolumeMatrix(work, params, num_params, matrix_format, out_volume_matrix); } +int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* out_attrib, std::uint32_t flags) { + return Ngs2GeomApply(listener, source, out_attrib, flags); +} + int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { return Ngs2GeomResetListenerParam(out_listener_param); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index ef5b874c41..379c277110 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -342,6 +342,15 @@ static void TestCustomFilter() { 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]); + + Ngs2CustomVoiceFilterParam unsupported{}; + unsupported.type = 2; + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, unsupported)); + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, Ngs2CustomVoiceFilterParam{}); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET, 1, &block}); + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + Require(out[0] > 0.4f); Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 91a377793b..9f1937abb9 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -60,22 +60,15 @@ static Ngs2GeomListenerParam Listener() { 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]); + for (std::size_t column = 0; column < 4; column++) Require(std::abs(work.matrix[row][column] - expected[row][column]) < 0.00001f); } } -static void TestDefaultListener() { +static void TestListenerTransform() { 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; + 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}}); @@ -83,11 +76,25 @@ static void TestDefaultListener() { 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}; + param.position = {3.0f, 4.0f, 5.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}}); + RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, 1, -5}, {0, 0, 0, 1}}); + + param.orient_front = {2.0f, 0.0f, 0.0f}; + param.orient_up = {1.0f, 3.0f, 0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{0, 0, -1, 5}, {0, 1, 0, -4}, {1, 0, 0, -3}, {0, 0, 0, 1}}); + + param.orient_front = {0.0f, 0.0f, -2.0f}; + param.orient_up = {0.0f, 3.0f, 0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 1) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, -1, 5}, {0, 0, 0, 1}}); + Require(work.coordinate == 1); + + param.orient_front = {0.0f, 0.0f, std::numeric_limits::max()}; + param.orient_up = {0.0f, std::numeric_limits::min(), 0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, 1, -5}, {0, 0, 0, 1}}); } static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t flags) { @@ -105,30 +112,12 @@ static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t fla 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); + RejectListener(¶m, 2); + RejectListener(¶m, 0xffffffffu); for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { param.sound_speed = value; RejectListener(¶m, 0); @@ -142,24 +131,59 @@ static void TestInvalidListener() { 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); - } - } + param = Listener(); + param.position.x = std::numeric_limits::infinity(); + RejectListener(¶m, 0); + param = Listener(); + param.velocity.z = std::numeric_limits::quiet_NaN(); + RejectListener(¶m, 0); + param = Listener(); + param.orient_front.y = std::numeric_limits::quiet_NaN(); + RejectListener(¶m, 0); +} + +static void TestGeomApply() { + Ngs2GeomListenerWork listener{}; + for (std::size_t row = 0; row < 4; row++) { + for (std::size_t column = 0; column < 4; column++) listener.matrix[row][column] = row == column ? 1.0f : 0.0f; } + + Ngs2GeomSourceParam source; + Require(sceNgs2GeomResetSourceParam(&source) == SCE_NGS2_OK); + source.position = {1.0f, 2.0f, 3.0f}; + source.min_level = -0.25f; + source.max_level = 0.5f; + source.matrix_format = 3; + + Ngs2GeomAttribute attribute; + std::memset(&attribute, 0x5a, sizeof(attribute)); + Require(sceNgs2GeomApply(&listener, &source, &attribute, 0) == SCE_NGS2_OK); + Require(attribute.pitch_ratio == 1.0f); + Require(attribute.a3d_attrib.position.x == 1.0f && attribute.a3d_attrib.position.y == 2.0f && attribute.a3d_attrib.position.z == 3.0f); + Require(attribute.a3d_attrib.volume == 0.5f); + Require(attribute.level[0] == 0.5f); + Require(attribute.level[1] == 0.0f); + Require(attribute.level[9] == 0.5f); + Require(attribute.level[18] == 0.5f); + Require(attribute.level[24] == 0.0f); + Require(attribute.level[63] == 0.0f); + + Require(sceNgs2GeomApply(&listener, &source, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + + source.matrix_format = 0; + std::memset(&attribute, 0x5a, sizeof(attribute)); + Require(sceNgs2GeomApply(&listener, &source, &attribute, 0) == SCE_NGS2_OK); + Require(attribute.level[0] == 0.5f); + Require(attribute.level[9] == 0.5f); + Require(attribute.level[16] == 0.0f); } int main() { TestLayouts(); TestResetListenerParam(); TestResetSourceParam(); - TestDefaultListener(); - TestUnsupportedListener(); + TestListenerTransform(); TestInvalidListener(); + TestGeomApply(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp new file mode 100644 index 0000000000..56246ca178 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -0,0 +1,48 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static void Status(int actual, int expected, const char* message) { + if (actual != expected) throw std::runtime_error(message); +} + +int main() { + try { + constexpr int systemError = static_cast(0x804A8201u); + constexpr int rackError = static_cast(0x804A8202u); + constexpr int outputError = static_cast(0x804A8010u); + constexpr uintptr_t sentinel = 0x5a5a; + uintptr_t value = sentinel; + Status(sceNgs2SystemGetUserData(0x1234, &value), systemError, "invalid system handle returned a non-PS5 error"); + Require(value == sentinel); + Status(sceNgs2SystemDestroy(0x1234, nullptr), systemError, "system destroy returned a non-PS5 handle error"); + Status(sceNgs2RackGetVoiceHandle(0x1234, 0, &value), rackError, "invalid rack handle returned a non-PS5 error"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(0x1234, 0, nullptr), rackError, "voice lookup checked output before the rack handle"); + Status(sceNgs2RackDestroy(0x1234, nullptr), rackError, "rack destroy returned a non-PS5 handle error"); + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_REVERB); + Status(sceNgs2SystemGetUserData(rack, &value), systemError, "rack handle was accepted as a system"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(system, 0, &value), rackError, "system handle was accepted as a rack"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(system, 0, nullptr), rackError, "wrong-kind rack did not take precedence over null output"); + Status(sceNgs2RackGetVoiceHandle(rack, 0, nullptr), outputError, "valid rack lost null-output validation"); + Status(sceNgs2RackGetVoiceHandle(rack, 0, &value), SCE_NGS2_OK, "valid voice lookup failed"); + Require(value != 0 && value != sentinel); + Status(sceNgs2RackDestroy(rack, nullptr), SCE_NGS2_OK, "valid rack destroy failed"); + value = sentinel; + Status(sceNgs2RackGetVoiceHandle(rack, 0, &value), rackError, "retired rack returned the wrong error"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(rack, 0, nullptr), rackError, "retired rack did not take precedence over null output"); + Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); + Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); + Require(value == sentinel); + std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index d65c46548b..15f0186700 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -41,8 +41,8 @@ static void TestErrorsAndInfo() { Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SAMPLER, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Ngs2SystemInfo info{}; - Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == static_cast(0x804A0230u)); - Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A0261u)); + Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == static_cast(0x804A8201u)); + Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A8202u)); const auto system = CreateSystem(); const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); @@ -297,6 +297,134 @@ static void TestPitchAndRepeat() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +struct PcmResetCallback { + std::vector pcm; + std::uint32_t flag; + int calls = 0; + std::uint32_t waveformFlags = SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET; + bool pause = false; +}; + +static void APS5_VABI ResetPcm(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + if (++callback.calls != 1) return; + Require(info->flag == callback.flag); + const Ngs2WaveformBlock block{0, callback.pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(callback.pcm.size()), 0, 0}; + Control(info->voice_handle, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, callback.pcm.data(), callback.waveformFlags, 1, &block}); + if (callback.pause) Event(info->voice_handle, SCE_NGS2_VOICE_EVENT_PAUSE); +} + +static void TestPcmCallbackReset() { + for (const auto flag : {SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT}) { + const auto system = CreateSystem(); + const std::vector pcm{16384}; + const auto sampler = Sampler(system, pcm, flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT ? 1 : 0); + Patch(sampler, Mastering(system, 1)); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 2.0f}); + PcmResetCallback callback{{}, flag}; + for (std::uint32_t frame = 0; frame < Grain * 2 + 2; ++frame) callback.pcm.push_back(static_cast(1000 + frame * 100)); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, ResetPcm, reinterpret_cast(&callback), flag, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto first = RenderI16(system); + Require(callback.calls == 1); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + if (state.num_decoded_samples != 1) std::fprintf(stderr, "PCM callback reset consumed %llu samples before the next grain, expected 1\n", static_cast(state.num_decoded_samples)); + Require(state.num_decoded_samples == 1 && state.decoded_data_size == sizeof(std::int16_t) && state.waveform_data == callback.pcm.data()); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(first[frame] == (frame == 0 ? 16384 : 0)); + const auto second = RenderI16(system); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == callback.pcm[frame * 2]); + Require(callback.calls == 1); + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == 1 + Grain * 2 && state.waveform_data == callback.pcm.data() + Grain * 2); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + +static void TestPcmCallbackRefill() { + for (const bool pause : {false, true}) { + const auto system = CreateSystem(); + const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + Patch(sampler, Mastering(system, 1)); + const std::int16_t pcm = 16384; + const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, 1, 0, 0}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, &pcm, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE, 1, &block}); + PcmResetCallback callback{{}, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0, 0, pause}; + for (std::uint32_t frame = 0; frame < Grain * 2; ++frame) callback.pcm.push_back(static_cast(1000 + frame * 100)); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, ResetPcm, reinterpret_cast(&callback), callback.flag, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto first = RenderI16(system); + Require(callback.calls == 1 && first.front() == pcm); + for (std::uint32_t frame = 1; frame < Grain; ++frame) Require(first[frame] == (pause ? 0 : callback.pcm[frame - 1])); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + const std::uint32_t consumed = pause ? 0 : Grain - 1; + Require(state.num_decoded_samples == consumed + 1 && state.waveform_data == callback.pcm.data() + consumed); + if (pause) Event(sampler, SCE_NGS2_VOICE_EVENT_RESUME); + const auto second = RenderI16(system); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == callback.pcm[consumed + frame]); + Require(callback.calls == 1); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + +struct PcmSetupCallback { + uintptr_t master; + std::uint32_t channels; + std::uint32_t flag; + std::vector pcm; + int calls = 0; +}; + +static void APS5_VABI SetupPcm(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + if (++callback.calls != 1) return; + Require(info->flag == callback.flag); + Control(info->voice_handle, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, callback.channels, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, callback.pcm.size() * sizeof(std::int16_t), 0, 0, Grain * 2, 0, 0}; + Control(info->voice_handle, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, callback.pcm.data(), 0, 1, &block}); + Patch(info->voice_handle, callback.master); + Event(info->voice_handle, SCE_NGS2_VOICE_EVENT_PLAY); +} + +static void TestPcmCallbackSetup() { + for (const auto channels : {2u, 8u}) { + for (const auto flag : {SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT}) { + const auto system = CreateSystem(); + const std::vector pcm{16384}; + const auto sampler = Sampler(system, pcm, flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT ? 1 : 0); + PcmSetupCallback callback{Mastering(system, channels), channels, flag, {}}; + for (std::uint32_t frame = 0; frame < Grain * 2; ++frame) { + for (std::uint32_t channel = 0; channel < channels; ++channel) callback.pcm.push_back(static_cast(1000 + frame * 25 + channel * 100)); + } + Patch(sampler, callback.master); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, SetupPcm, reinterpret_cast(&callback), flag, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector output(Grain * channels, -1); + const Ngs2RenderBufferInfo info{output.data(), output.size() * sizeof(std::int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(callback.calls == 1 && std::all_of(output.begin(), output.end(), [](std::int16_t sample) { return sample == 0; })); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == 0 && state.decoded_data_size == 0 && state.waveform_data == callback.pcm.data()); + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(callback.calls == 1 && std::equal(output.begin(), output.end(), callback.pcm.begin())); + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == Grain && state.decoded_data_size == Grain * channels * sizeof(std::int16_t)); + Require(state.waveform_data == callback.pcm.data() + Grain * channels); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } + } +} + static void TestSubmixerMatrix() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -753,23 +881,57 @@ static void TestStereoIntoSurround() { static void TestLock() { const auto system = CreateSystem(); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); Require(sceNgs2SystemUnlock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); - Ngs2SystemInfo info{}; - std::thread free([&] { Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); }); - free.join(); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + Ngs2SystemInfo info{}; Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::atomic otherDone = false; + std::thread other([&] { + Ngs2SystemInfo otherInfo{}; + std::uint32_t flags = 0; + Require(sceNgs2SystemGetInfo(system, &otherInfo, sizeof(otherInfo)) == SCE_NGS2_OK); + Require(sceNgs2VoiceGetStateFlags(voice, &flags) == SCE_NGS2_OK); + std::vector out(Grain * 2, -1); + const Ngs2RenderBufferInfo buffer{out.data(), out.size() * sizeof(std::int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + Require(sceNgs2SystemRender(system, &buffer, 1) == SCE_NGS2_OK); + Require(std::all_of(out.begin(), out.begin() + Grain, [](std::int16_t sample) { return sample == 0; })); + Require(std::all_of(out.begin() + Grain, out.end(), [](std::int16_t sample) { return sample == -1; })); + Require(sceNgs2SystemGetInfo(system, &otherInfo, sizeof(otherInfo)) == SCE_NGS2_OK && otherInfo.render_count == 0); + otherDone = true; + }); + for (int i = 0; i < 200 && !otherDone; i++) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(otherDone); + other.join(); + RenderI16(system); + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.render_count == 1); + std::atomic done = false; std::thread blocked([&] { - Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); done = true; + RenderI16(system); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); }); std::this_thread::sleep_for(std::chrono::milliseconds(100)); Require(!done); Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!done); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); blocked.join(); Require(done); + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.render_count == 2); + + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::thread unlocker([&] { Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); }); + unlocker.join(); + std::thread renderer([&] { RenderI16(system); }); + renderer.join(); + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.render_count == 3); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -970,6 +1132,9 @@ int main() { TestReverbSetup(); TestSampleRate(); TestUserData(); + TestPcmCallbackReset(); + TestPcmCallbackRefill(); + TestPcmCallbackSetup(); TestMasteringGain(); TestMatrixLevelClamp(); TestStereoIntoSurround(); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index fdc379e560..4cc96707d1 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -42,6 +42,7 @@ 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*); +int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork*, const Ngs2GeomSourceParam*, Ngs2GeomAttribute*, std::uint32_t); } inline void Check(bool value, int line) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp new file mode 100644 index 0000000000..2b7453a1a0 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp @@ -0,0 +1,91 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include + +namespace { + +union RackOption { + Ngs2RackOption common; + Ngs2SamplerRackOption sampler; + Ngs2SubmixerRackOption submixer; + Ngs2MasteringRackOption mastering; + Ngs2ReverbRackOption reverb; +}; + +std::size_t OptionSize(std::uint32_t rackId) { + switch (rackId) { + case SCE_NGS2_RACK_ID_SAMPLER: return sizeof(Ngs2SamplerRackOption); + case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); + case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); + default: return sizeof(Ngs2ReverbRackOption); + } +} + +uintptr_t CreateSingleVoiceRack(uintptr_t system, std::uint32_t rackId) { + RackOption option{}; + option.common.size = OptionSize(rackId); + option.common.max_grain_samples = 512; + option.common.max_voices = 1; + option.common.max_input_delay_blocks = 1; + option.common.max_matrices = 1; + option.common.max_ports = 8; + switch (rackId) { + case SCE_NGS2_RACK_ID_SAMPLER: + option.sampler.max_channel_works = 1; + option.sampler.max_waveform_blocks = 1; + break; + case SCE_NGS2_RACK_ID_SUBMIXER: + option.submixer.max_channels = 2; + option.submixer.max_inputs = 1; + break; + case SCE_NGS2_RACK_ID_MASTERING: + option.mastering.max_channels = 2; + break; + default: + option.reverb.max_channels = 2; + option.reverb.reverb_size = 1; + break; + } + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(rackId, &option.common, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, rackId, &option.common, &info, &rack) == SCE_NGS2_OK && rack != 0); + return rack; +} + +void CheckOutOfRange(uintptr_t rack, std::uint32_t maxVoices) { + uintptr_t last = 0; + Require(sceNgs2RackGetVoiceHandle(rack, maxVoices - 1, &last) == SCE_NGS2_OK && last != 0); + for (const auto index : std::array{maxVoices, maxVoices + 1, std::numeric_limits::max()}) { + uintptr_t output = 0x5a5a; + Require(sceNgs2RackGetVoiceHandle(rack, index, &output) == SCE_NGS2_ERROR_INVALID_VOICE_INDEX); + Require(output == 0x5a5a); + } + Require(sceNgs2RackGetVoiceHandle(rack, maxVoices, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + uintptr_t again = 0; + Require(sceNgs2RackGetVoiceHandle(rack, maxVoices - 1, &again) == SCE_NGS2_OK && again == last); +} + +} + +int main() { + const auto system = CreateSystem(); + for (const auto rackId : std::array{SCE_NGS2_RACK_ID_SAMPLER, SCE_NGS2_RACK_ID_SUBMIXER, + SCE_NGS2_RACK_ID_MASTERING, SCE_NGS2_RACK_ID_REVERB}) { + const auto rack = CreateSingleVoiceRack(system, rackId); + CheckOutOfRange(rack, 1); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + } + const auto sampler = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + Ngs2RackInfo info{}; + Require(sceNgs2RackGetInfo(sampler, &info, sizeof(info)) == SCE_NGS2_OK && info.max_voices == 256); + CheckOutOfRange(sampler, info.max_voices); + uintptr_t output = 0x5a5a; + Require(sceNgs2RackGetVoiceHandle(0x1234, 0, &output) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && output == 0x5a5a); + Require(sceNgs2RackDestroy(sampler, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + std::puts("NGS2 voice index: PASS"); +} diff --git a/core/libs/prx/libSceNpManager/CMakeLists.txt b/core/libs/prx/libSceNpManager/CMakeLists.txt index f0f629d89b..cd99c36b34 100644 --- a/core/libs/prx/libSceNpManager/CMakeLists.txt +++ b/core/libs/prx/libSceNpManager/CMakeLists.txt @@ -20,7 +20,8 @@ if(BUILD_TESTING) endif() add_test_executable(guest_np_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpManager.cpp) target_include_directories(guest_np_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_manager_tests PRIVATE libSceNpManager) + target_link_libraries(guest_np_manager_tests PRIVATE libSceNpManager libc) + configure_windows_unwind(guest_np_manager_tests) add_test(NAME guest_np_manager COMMAND guest_np_manager_tests) if(WIN32) set_tests_properties(guest_np_manager PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index a384c22023..848133acd5 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -238,9 +238,9 @@ int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { return 0; } -int APS5_VABI sceNpGetUserIdByAccountId() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpGetUserIdByAccountId(uint64_t account_id, int* user_id) { + if (account_id == 0 || !user_id) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpGetSandboxInfo(void* sandbox_info) { @@ -249,4 +249,18 @@ int APS5_VABI sceNpGetSandboxInfo(void* sandbox_info) { return 0; } +APS5_EXPORT("8weiQyIdI3A", sceNpUnknown_8weiQyIdI3A); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer) { + (void)userId; + (void)buffer; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("WxLcN76q19o", sceNpUnknown_WxLcN76q19o); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpPartner001/Export.cpp b/core/libs/prx/libSceNpPartner001/Export.cpp index 9d2e089775..d4dbeeefd9 100644 --- a/core/libs/prx/libSceNpPartner001/Export.cpp +++ b/core/libs/prx/libSceNpPartner001/Export.cpp @@ -21,4 +21,12 @@ int APS5_VABI sceNpEAAccessTerminate(void) { return 0; } +int APS5_VABI sceNpHasEAAccessSubscription(int userId, bool* result) { + (void)userId; + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (result == nullptr) throw std::logic_error(std::string(__func__) + ": null result"); + *result = false; + return 0; +} + } diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index d3086ee40a..e75842f866 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -99,4 +99,14 @@ int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { return 0; } +int APS5_VABI sceNpSessionSignalingGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index 74048df024..197f834f71 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -35,7 +36,12 @@ int APS5_VABI sceNpTrophy2UnregisterUnlockCallback() { } -int APS5_VABI sceNpTrophy2GetRewardIcon(void) { +int APS5_VABI sceNpTrophy2GetRewardIcon(int context, int handle, int reward_id, void* buffer, size_t* size) { + (void)context; + (void)handle; + (void)reward_id; + (void)buffer; + (void)size; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index f36474b481..1157934220 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -119,9 +119,9 @@ int APS5_VABI sceNpWebApi2PushEventCreateHandle(int lib_ctx_id) { } int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libCtxId, int handleId) { - (void)libCtxId; (void)handleId; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libCtxId)) throw std::invalid_argument("sceNpWebApi2PushEventAbortHandle: unknown context"); return 0; } diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp index 0e31fb9e0d..eef8d86d52 100644 --- a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -12,10 +12,12 @@ #include #include #include +#include #include extern "C" { #include +#include } namespace { @@ -54,13 +56,15 @@ public: int CreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { if (!context || !state) fail("null context or state"); GetSize(channels); - if (sampleRate != 48000) fail("sample rates other than 48000 Hz are not implemented"); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) + fail("sample rates other than 8000, 12000, 16000, 24000 and 48000 Hz are not supported"); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); if (decoders.contains(state)) fail("state already owns a decoder"); auto decoder = std::make_unique(); decoder->context = *context; decoder->channels = channels; + decoder->sampleRate = sampleRate; const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); if (!codec) fail("FFmpeg Opus decoder unavailable"); decoder->codec = avcodec_alloc_context3(codec); @@ -68,6 +72,13 @@ public: av_channel_layout_default(&decoder->codec->ch_layout, channels); decoder->codec->sample_rate = 48000; if (avcodec_open2(decoder->codec, codec, nullptr) < 0) fail("cannot open FFmpeg decoder"); + if (sampleRate != 48000) { + AVChannelLayout layout{}; + av_channel_layout_default(&layout, channels); + if (swr_alloc_set_opts2(&decoder->resampler, &layout, AV_SAMPLE_FMT_FLT, sampleRate, &layout, AV_SAMPLE_FMT_FLT, 48000, 0, nullptr) < 0 || + swr_init(decoder->resampler) < 0) + fail("cannot open FFmpeg resampler"); + } decoders.emplace(state, std::move(decoder)); return 0; } @@ -80,7 +91,7 @@ public: if (found == decoders.end()) fail("unknown decoder state"); auto& decoder = *found->second; if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); - const int samples = packetSamples(data, bytes); + const int samples = packetSamples(data, bytes) / (48000 / decoder.sampleRate); const int outputBytes = samples * decoder.channels * 2; if (capacity / 2 * 2 < outputBytes) fail("PCM buffer is too small"); std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); @@ -93,12 +104,26 @@ public: if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) fail("unexpected FFmpeg PCM format"); + std::vector decoded; + decoded.reserve(static_cast(frame->nb_samples) * decoder.channels); for (int sample = 0; sample < frame->nb_samples; ++sample) { for (int channel = 0; channel < decoder.channels; ++channel) { const auto* source = reinterpret_cast(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - decoder.pending.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); + decoded.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); } } + if (decoder.resampler) { + const int capacitySamples = swr_get_out_samples(decoder.resampler, frame->nb_samples); + if (capacitySamples < 0) fail("FFmpeg resampler rejected the frame"); + std::vector resampled(static_cast(capacitySamples) * decoder.channels); + auto* output = reinterpret_cast(resampled.data()); + const auto* input = reinterpret_cast(decoded.data()); + const int converted = swr_convert(decoder.resampler, &output, capacitySamples, &input, frame->nb_samples); + if (converted < 0) fail("FFmpeg resampler rejected the frame"); + resampled.resize(static_cast(converted) * decoder.channels); + decoded = std::move(resampled); + } + decoder.pending.insert(decoder.pending.end(), decoded.begin(), decoded.end()); const auto count = static_cast(samples) * decoder.channels; if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); for (std::size_t index = 0; index < count; ++index) { @@ -119,9 +144,14 @@ private: struct Decoder { std::uint32_t context; int channels; + int sampleRate; AVCodecContext* codec = nullptr; + SwrContext* resampler = nullptr; std::vector pending; - ~Decoder() { avcodec_free_context(&codec); } + ~Decoder() { + swr_free(&resampler); + avcodec_free_context(&codec); + } }; const char* name; diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index c65a5df868..3dd4c0650a 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -14,8 +15,11 @@ namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast(0x80920008); +constexpr int PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT = static_cast(0x80920005); +constexpr int PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT = static_cast(0x809200ff); bool g_opened = false; +std::atomic_bool g_initialized{false}; bool ValidPort(int userId, int type, int index) { const bool personalPort = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL; @@ -143,6 +147,7 @@ int APS5_VABI scePadSetParticularMode(bool mode) { int APS5_VABI scePadInit_nid_postfix(void) { Pad::Initialize(); + g_initialized.store(true, std::memory_order_release); return PAD_OK; } @@ -312,8 +317,8 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabl APS5_EXPORT("n3kSX62fgNo", scePadUnknown_n3kSX62fgNo); int APS5_VABI scePadUnknown_n3kSX62fgNo() { - NotImplemented_nid_no_patch(__func__); - return 0; + if (g_opened) NotImplemented_nid_no_patch(__func__); + return g_initialized.load(std::memory_order_acquire) ? PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT : PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT; } int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 7433fd9f49..5ee3b1fb73 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace Pad { @@ -15,11 +16,13 @@ public: std::uint32_t buttons = 0; bool hasSticks = false; std::array sticks{}; + bool touchLeft = false; + bool touchRight = false; bool operator==(const Input&) const = default; }; - explicit InputCommandFile(const char* path) : path(path != nullptr ? path : "") { + explicit InputCommandFile(const std::filesystem::path& path) : path(path) { if (!this->path.empty()) lastSequence = read().sequence; } @@ -64,10 +67,12 @@ private: const auto parsed = std::from_chars(first, last, values[i]); if (first == last || parsed.ec != std::errc{} || parsed.ptr != last) return {}; } - if (values[0] == 0 || (values[1] & ~0x10fffeull) != 0 || values[2] == 0 || values[2] > 5000) return {}; + if (values[0] == 0 || (values[1] & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; command.sequence = values[0]; - command.input.buttons = static_cast(values[1]); + command.input.buttons = static_cast(values[1] & 0x10fffeull); + command.input.touchLeft = (values[1] & TouchLeftBit) != 0; + command.input.touchRight = (values[1] & TouchRightBit) != 0; command.durationMs = static_cast(values[2]); for (int i = 3; i < count; ++i) { if (values[i] > 255) return {}; @@ -77,7 +82,10 @@ private: return command; } - std::string path; + static constexpr std::uint64_t TouchLeftBit = 0x200000; + static constexpr std::uint64_t TouchRightBit = 0x400000; + + std::filesystem::path path; std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; std::uint64_t until = 0; diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 68e501a384..53336f6a47 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -48,6 +48,7 @@ public: InputRequests HandleEvent(const SDL_Event& event, Clock::time_point now); bool Advance(Clock::time_point now); void SetMouseMode(bool enabled); + void SetTitleReadsMouse(bool reads); void Release(); bool MouseMode() const; PadInputState Compose(const PadInputState& controller) const; @@ -61,6 +62,7 @@ private: Clock::time_point lastAdvance{}; bool advanced = false; bool mouseMode = false; + bool titleReadsMouse = false; }; std::uint8_t StickByte(double deflection); diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index a915e25c21..21f63c8232 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -179,10 +179,10 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin } ++touchNum; } - if (touchNum == 0 && (state.touchLeft || state.touchRight)) { + if (touchNum == 0 && (state.touchLeft || state.touchRight || injected.touchLeft || injected.touchRight)) { data.buttons |= 0x100000; touchNum = 1; - data.touch_data_touch0_x = state.touchRight ? 1440 : 480; + data.touch_data_touch0_x = state.touchRight || injected.touchRight ? 1440 : 480; data.touch_data_touch0_y = 471; data.touch_data_touch0_id = 0; } @@ -191,7 +191,14 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin } Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { - static Pad::InputCommandFile commands(std::getenv("APS5_PAD_INPUT_FILE")); + static Pad::InputCommandFile commands([] { +#ifdef _WIN32 + const auto* path = _wgetenv(L"APS5_PAD_INPUT_FILE"); +#else + const auto* path = std::getenv("APS5_PAD_INPUT_FILE"); +#endif + return path ? std::filesystem::path(path) : std::filesystem::path{}; + }()); static Pad::InputCommandFile::Input lastInjected; const auto injected = commands.Poll(now); if (injected != lastInjected) { diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 38d6a3621a..2bc5bfc67c 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include #include #include @@ -86,11 +87,30 @@ static const ChunkTable& Chunks() { return chunks; } +enum class UnlistedChunkAnswer { Stop, Installed, Absent }; + +static UnlistedChunkAnswer UnlistedChunks() { + static const UnlistedChunkAnswer answer = [] { + const char* text = std::getenv("APS5_PLAYGO_UNLISTED_CHUNKS"); + if (text == nullptr || text[0] == '\0') return UnlistedChunkAnswer::Stop; + const std::string value(text); + if (value == "installed") return UnlistedChunkAnswer::Installed; + if (value == "absent") return UnlistedChunkAnswer::Absent; + throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent, not \"" + value + "\""); + }(); + return answer; +} + static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); if (chunks.defined) return chunks.ids.contains(id); if (chunks.scenario || id == 0) return true; - throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked"); + switch (UnlistedChunks()) { + case UnlistedChunkAnswer::Installed: return true; + case UnlistedChunkAnswer::Absent: return false; + case UnlistedChunkAnswer::Stop: break; + } + throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump"); } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { diff --git a/core/libs/prx/libScePlayGoDialog/Export.cpp b/core/libs/prx/libScePlayGoDialog/Export.cpp index 16615b02ee..09a8df031c 100644 --- a/core/libs/prx/libScePlayGoDialog/Export.cpp +++ b/core/libs/prx/libScePlayGoDialog/Export.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" diff --git a/core/libs/prx/libScePsml_debug/CMakeLists.txt b/core/libs/prx/libScePsml_debug/CMakeLists.txt index ae4867fdcc..aceb0af750 100644 --- a/core/libs/prx/libScePsml_debug/CMakeLists.txt +++ b/core/libs/prx/libScePsml_debug/CMakeLists.txt @@ -4,7 +4,8 @@ add_sce_psml_library(libScePsml_debug) if(BUILD_TESTING) add_test_executable(guest_psml_tests ${LIBS_INCLUDE_DIR}/tests/GuestPsml.cpp) target_include_directories(guest_psml_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_psml_tests PRIVATE libScePsml_debug) + target_link_libraries(guest_psml_tests PRIVATE libScePsml_debug libc) + configure_windows_unwind(guest_psml_tests) add_test(NAME guest_psml COMMAND guest_psml_tests) if(WIN32) set_tests_properties(guest_psml PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 758ceaffcf..e8cc8f8b7a 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -1,11 +1,98 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr std::int32_t SCE_PSML_ERROR_NOT_INITIALIZED = static_cast(0x8A810001); +constexpr std::uint64_t MfsrBlockBytes = 0x200000; +constexpr std::uint64_t MfsrBlockCount = 4; + +struct MfsrRequirement { + std::uint64_t blockBytes; + std::uint64_t alignment; + std::uint64_t blockCount; +}; + +struct MfsrBlock { + std::uint64_t address; + std::uint64_t bytes; +}; + +struct MfsrSharedResourcesParam { + std::uint32_t type; + std::uint32_t reserved; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint64_t reserved2; +}; + +struct MfsrContextParam { + std::uint32_t reserved0[2]; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint32_t reserved1[8]; + void* sharedResources; +}; + +static_assert(sizeof(MfsrRequirement) == 0x18); +static_assert(sizeof(MfsrSharedResourcesParam) == 0x20); +static_assert(offsetof(MfsrContextParam, blocks) == 0x08); +static_assert(offsetof(MfsrContextParam, blockCount) == 0x10); +static_assert(offsetof(MfsrContextParam, sharedResources) == 0x38); + +enum class MfsrObjectKind { SharedResources, Context }; + +struct MfsrObject { + MfsrObjectKind kind; + void* sharedResources; +}; + +std::atomic g_mfsrInitialized{false}; +std::mutex g_mfsrMutex; +std::unordered_map g_mfsrObjects; + +void RequireMfsr(bool condition, const char* function, const char* what) { + if (!condition) throw std::runtime_error(std::string(function) + ": " + what); +} + +void RequireBlocks(const MfsrBlock* blocks, std::uint64_t blockCount, const char* function) { + RequireMfsr(blockCount >= MfsrBlockCount, function, "fewer memory blocks than the requirement"); + RequireMfsr(blocks != nullptr, function, "null memory block list"); + for (std::uint64_t i = 0; i < MfsrBlockCount; ++i) { + RequireMfsr(blocks[i].bytes >= MfsrBlockBytes && blocks[i].address % MfsrBlockBytes == 0, function, "memory block smaller or less aligned than the requirement"); + } +} + +void* CreateMfsrObject(MfsrObjectKind kind, void* sharedResources, const char* function) { + std::lock_guard lock(g_mfsrMutex); + if (kind == MfsrObjectKind::Context) { + const auto found = g_mfsrObjects.find(sharedResources); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == MfsrObjectKind::SharedResources, function, "shared resources handle was not created by this library"); + } + auto* object = new MfsrObject{kind, sharedResources}; + g_mfsrObjects.emplace(object, object); + return object; +} + +void ReleaseMfsrObject(void* handle, MfsrObjectKind kind, const char* function) { + std::lock_guard lock(g_mfsrMutex); + const auto found = g_mfsrObjects.find(handle); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == kind, function, "handle was not created by this library"); + if (kind == MfsrObjectKind::SharedResources) { + for (const auto& [key, object] : g_mfsrObjects) { + RequireMfsr(object->sharedResources != handle, function, "shared resources still used by a context"); + } + } + delete found->second; + g_mfsrObjects.erase(found); +} } @@ -14,12 +101,14 @@ extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param) { (void)requirement; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } std::int32_t APS5_VABI scePsmlMfsrCreateContext1100(void** context, const void* param) { (void)context; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } @@ -27,39 +116,79 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* (void)context; (void)commandBuffer; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsrCreateContext1300() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrInit() { + g_mfsrInitialized = true; return 0; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(MfsrRequirement* requirement, const MfsrSharedResourcesParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); + RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; return 0; } -int APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const MfsrSharedResourcesParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(sharedResources != nullptr && param != nullptr, __func__, "null handle or parameter"); + RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + RequireBlocks(param->blocks, param->blockCount, __func__); + *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr, __func__); return 0; } -int APS5_VABI scePsmlMfsrInit() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrReleaseSharedResources(void* sharedResources) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(sharedResources, MfsrObjectKind::SharedResources, __func__); return 0; } +std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(MfsrRequirement* requirement, const MfsrContextParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const MfsrContextParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(context != nullptr && param != nullptr, __func__, "null handle or parameter"); + RequireBlocks(param->blocks, param->blockCount, __func__); + *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(context, MfsrObjectKind::Context, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param) { + (void)context; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; +} + int APS5_VABI scePsmlMfsrGetDispatchMfsrPacketSizeInDwords() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePsmlMfsrSelectConfig() { NotImplemented_nid_no_patch(__func__); return 0; @@ -75,39 +204,25 @@ int APS5_VABI scePsmlMfsrRequestCapture() { return 0; } -int APS5_VABI scePsmlMfsrReleaseContext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePsmlMfsrIsCaptureInProgress() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrCreateSharedResources() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrCreateContext800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrReleaseSharedResources() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; } int APS5_VABI scePsmlMfsr2ReleaseSharedResources() { diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 23369d1158..529be85908 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -31,6 +31,11 @@ int APS5_VABI sceRazorCpuPushMarker(const char* label, uint32_t color, uint32_t return 0; } +int APS5_VABI sceRazorCpuNamedSync(const char* label) { + (void)label; + return 0; +} + int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, uint32_t color, uint32_t flags) { (void)name; (void)color; diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 0744be6f86..b9119c2159 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -372,8 +372,10 @@ int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - const std::uint64_t utc = currentTick(); - *time = fromTick(utc + static_cast(localOffsetSeconds(utc) * TICKS_PER_SECOND)); + RtcTick local{}; + const RtcTick utc{currentTick()}; + if (const int result = addTicks(&local, &utc, localOffsetSeconds(utc.tick), TICKS_PER_SECOND); result != 0) return result; + *time = fromTick(local.tick); return 0; } diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 2bf31d06e5..0dcf019ce6 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -286,4 +286,24 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } +int APS5_VABI sceRudpPollCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 3a75d7875f..03a86db6dd 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -217,6 +217,7 @@ int APS5_VABI sceSaveDataCommit(const SaveDataCommitParam* param) { int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { (void)size; + if (g_initializations == 0) return SAVE_DATA_ERROR_NOT_INITIALIZED; return g_transaction_counter.fetch_add(1); } @@ -271,6 +272,7 @@ int APS5_VABI sceSaveDataDelete(const SaveDataDelete* del) { int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { (void)resource; + if (g_initializations == 0) return SAVE_DATA_ERROR_NOT_INITIALIZED; return SAVE_DATA_OK; } @@ -403,7 +405,7 @@ int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { if (mount_point == nullptr || info == nullptr) { - throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); @@ -425,7 +427,7 @@ int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint* mount_point, Sav static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, void* param_buf, size_t param_buf_size, size_t* got_size) { if (mount_point == nullptr || param_buf == nullptr) { - throw std::runtime_error("sceSaveDataGetParam: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); @@ -529,11 +531,11 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result } const auto* nameEnd = static_cast(std::memchr(mount->dir_name->data, '\0', sizeof(mount->dir_name->data))); if (nameEnd == nullptr) { - throw std::runtime_error("sceSaveDataMount3: unterminated directory name"); + return SAVE_DATA_ERROR_PARAMETER; } const std::string dirName(mount->dir_name->data, static_cast(nameEnd - mount->dir_name->data)); if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { - throw std::runtime_error("sceSaveDataMount3: invalid directory name"); + return SAVE_DATA_ERROR_PARAMETER; } const std::string real_path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); @@ -576,7 +578,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) { const int rc = mount3(mount, mount_result); - SAVEDATA_TRACE("mount3 user=%d dir=%s mode=0x%x blocks=%llu -> 0x%08x point=%s status=%u", mount != nullptr ? mount->user_id : -1, mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); + SAVEDATA_TRACE("mount3 user=%d dir=%.*s mode=0x%x blocks=%llu -> 0x%08x point=%.*s status=%u", mount != nullptr ? mount->user_id : -1, static_cast(sizeof(SceSaveDataDirName::data)), mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), static_cast(sizeof(SaveDataMountPoint::data)), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); return rc; } diff --git a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp index 3c834f7d9c..54c4fd0926 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp @@ -33,6 +33,9 @@ int APS5_VABI sceSaveDataDialogUpdateStatus(void) { } int APS5_VABI sceSaveDataDialogGetResult(void* result) { + if (g_status != SAVE_DATA_DIALOG_STATUS_FINISHED) { + return SAVE_DATA_DIALOG_ERROR_NOT_FINISHED; + } if (result == nullptr) { return SAVE_DATA_DIALOG_ERROR_ARG_NULL; } @@ -76,6 +79,9 @@ int APS5_VABI sceSaveDataDialogOpen(const void* param) { int APS5_VABI sceSaveDataDialogClose(const void* closeParam) { (void)closeParam; + if (g_status != SAVE_DATA_DIALOG_STATUS_RUNNING) { + return SAVE_DATA_DIALOG_ERROR_NOT_RUNNING; + } g_status = SAVE_DATA_DIALOG_STATUS_FINISHED; return SAVE_DATA_DIALOG_OK; } @@ -85,6 +91,9 @@ int APS5_VABI sceSaveDataDialogIsReadyToDisplay(void) { } int APS5_VABI sceSaveDataDialogTerminate(void) { + if (g_status == SAVE_DATA_DIALOG_STATUS_NONE) { + return SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED; + } g_status = SAVE_DATA_DIALOG_STATUS_NONE; g_mode = 0; g_user_data = nullptr; @@ -95,12 +104,18 @@ int APS5_VABI sceSaveDataDialogTerminate(void) { int APS5_VABI sceSaveDataDialogProgressBarInc(int target, std::uint32_t delta) { (void)target; (void)delta; + if (g_status != SAVE_DATA_DIALOG_STATUS_RUNNING) { + return SAVE_DATA_DIALOG_ERROR_NOT_RUNNING; + } return SAVE_DATA_DIALOG_OK; } int APS5_VABI sceSaveDataDialogProgressBarSetValue(int target, std::uint32_t rate) { (void)target; (void)rate; + if (g_status != SAVE_DATA_DIALOG_STATUS_RUNNING) { + return SAVE_DATA_DIALOG_ERROR_NOT_RUNNING; + } return SAVE_DATA_DIALOG_OK; } diff --git a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp index 3259b8d482..7a8eda2bd0 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp +++ b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp @@ -7,7 +7,9 @@ constexpr int SAVE_DATA_DIALOG_OK = 0; constexpr int SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003); constexpr int SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004); +constexpr int SAVE_DATA_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005); constexpr int SAVE_DATA_DIALOG_ERROR_INVALID_STATE = static_cast(0x80B80006); +constexpr int SAVE_DATA_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000B); constexpr int SAVE_DATA_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000D); constexpr int SAVE_DATA_DIALOG_STATUS_NONE = 0; diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index b12d6bb913..0f9622a8ac 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -146,6 +147,15 @@ struct Snapshot { std::size_t count; }; +struct CertNameEntry { + std::string oid; + std::string value; +}; + +struct CertName { + std::vector entries; +}; + struct Context { Pool pool; std::vector> authorities; @@ -153,17 +163,24 @@ struct Context { std::vector> certificateHandles; std::unordered_map caLists; std::unordered_map caCerts; + std::unordered_map> certNames; explicit Context(std::size_t size) : pool{size} {} }; using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); +constexpr int SslVersionNone = 0; +constexpr int SslVersionTls12 = 5; + struct SslConnection { int context; int socket; std::string host; SslVerifyCallback verifyCallback; void* verifyArgument; + std::uint32_t verifyOptions = 0; + int minimumVersion = SslVersionNone; + std::string alpn; }; struct Runtime { @@ -236,6 +253,49 @@ std::size_t CertificateCount(const Context& context) { return count; } +const mbedtls_x509_crt* RequireCertificate(const char* function, int sslCtxId, void* cert) { + if (!cert) throw std::invalid_argument(std::string(function) + ": null certificate"); + auto& context = GetContext(sslCtxId); + for (const auto& handle : context.certificateHandles) { + if (handle.get() == cert) { + if (handle->owner.expired() || !handle->certificate) { + throw std::invalid_argument(std::string(function) + ": expired certificate"); + } + return handle->certificate; + } + } + throw std::invalid_argument(std::string(function) + ": unknown certificate"); +} + +void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { + std::lock_guard lock(State().mutex); + auto& context = GetContext(sslCtxId); + const auto* crt = RequireCertificate(function, sslCtxId, sslCert); + const auto* name = subject ? &crt->subject : &crt->issuer; + auto certName = std::make_unique(); + for (const auto* node = name; node; node = node->next) { + if (!node->oid.p || node->oid.len == 0) continue; + std::string oid; + const char* shortName = nullptr; + if (mbedtls_oid_get_attr_short_name(&node->oid, &shortName) == 0 && shortName) { + oid = shortName; + } else { + char numeric[64] = {}; + if (mbedtls_oid_get_numeric_string(numeric, sizeof(numeric), &node->oid) > 0) { + oid = numeric; + } + } + std::string value; + if (node->val.p && node->val.len > 0) { + value.assign(reinterpret_cast(node->val.p), node->val.len); + } + certName->entries.push_back({std::move(oid), std::move(value)}); + } + auto* handle = certName.get(); + context.certNames.emplace(handle, std::move(certName)); + return handle; +} + } extern "C" { @@ -463,48 +523,105 @@ int APS5_VABI sceSslClose() { return 0; } -int APS5_VABI sceSslGetSerialNumber() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslFreeSslCertName(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* sboData, std::size_t* sboLen) { + if (!sboLen) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + const auto reqLen = crt->serial.len; + if (!sboData || *sboLen == 0) { + *sboLen = reqLen; + return 0; + } + if (*sboLen < reqLen) { + *sboLen = reqLen; + return ErrorInvalidArg; + } + std::memcpy(sboData, crt->serial.p, reqLen); + *sboLen = reqLen; return 0; } -int APS5_VABI sceSslDisableVerifyOption(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { + if (!certName) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslFreeSslCertName: unknown cert name"); + context.certNames.erase(found); return 0; } -int APS5_VABI sceSslGetAlpnSelected(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).verifyOptions |= option; return 0; } -int APS5_VABI sceSslSetAlpn(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslDisableVerifyOption(int sslConnectionId, std::uint32_t option) { + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).verifyOptions &= ~option; return 0; } -int APS5_VABI sceSslSetMinSslVersion(void) { - NotImplemented_nid_no_patch(__func__); +const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId); + return nullptr; +} + +int APS5_VABI sceSslSetAlpn(int sslConnectionId, const char* proto) { + if (!proto) throw std::invalid_argument("sceSslSetAlpn: null protocol list"); + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).alpn = proto; return 0; } -int APS5_VABI sceSslGetIssuerName(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { + if (version < SslVersionNone || version > SslVersionTls12) { + throw std::invalid_argument("sceSslSetMinSslVersion: unknown version " + std::to_string(version)); + } + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).minimumVersion = version; return 0; } -int APS5_VABI sceSslGetNameEntryCount(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceSslGetIssuerName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, false); } -int APS5_VABI sceSslGetNameEntryInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslGetNameEntryCount(int sslCtxId, void* certName) { + if (!certName) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); + return static_cast(found->second->entries.size()); +} + +int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, char* oidname, std::size_t maxOidnameLen, + char* value, std::size_t maxValueLen, std::size_t* valueLen) { + if (!certName) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); + const auto& entries = found->second->entries; + if (entryNum < 0 || static_cast(entryNum) >= entries.size()) return ErrorInvalidArg; + const auto& entry = entries[static_cast(entryNum)]; + if (oidname && maxOidnameLen > 0) { + const auto copyLen = std::min(entry.oid.size(), maxOidnameLen - 1); + std::memcpy(oidname, entry.oid.data(), copyLen); + oidname[copyLen] = '\0'; + } + if (value && maxValueLen > 0) { + const auto copyLen = std::min(entry.value.size(), maxValueLen - 1); + std::memcpy(value, entry.value.data(), copyLen); + value[copyLen] = '\0'; + } + if (valueLen) *valueLen = entry.value.size(); return 0; } @@ -513,14 +630,8 @@ int APS5_VABI sceSslGetPem(void) { return 0; } -int APS5_VABI sceSslGetSubjectName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslEnableVerifyOption(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceSslGetSubjectName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, true); } } diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 8ed9239477..a8acb385cb 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -38,23 +39,86 @@ void* openHostModule(const char* name) { return dlopen_nid_postfix(relative.c_str(), kRtldNow); } -bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { #ifdef _WIN32 - MEMORY_BASIC_INFORMATION mbi{}; - if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) { - return false; - } +struct ImageRegion { + std::uint64_t end = 0; + std::uintptr_t image = 0; + DWORD stamp = 0; + DWORD size = 0; + std::string name; +}; + +std::mutex gRegionsMutex; +std::map gRegions; + +bool imageIdentity(std::uintptr_t image, DWORD& stamp, DWORD& size) { + const auto* dos = reinterpret_cast(image); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto* nt = reinterpret_cast(image + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + stamp = nt->FileHeader.TimeDateStamp; + size = nt->OptionalHeader.SizeOfImage; + return true; +} + +std::uintptr_t loaderImage(std::uint64_t addr) { + PVOID image = nullptr; + if (RtlPcToFileHeader(reinterpret_cast(addr), &image) == nullptr) return 0; + return reinterpret_cast(image); +} + +void writeUnwindRegion(std::uint64_t begin, std::uint64_t size, const std::string& name, ModuleInfoForUnwind* info) { info->st_size = sizeof(ModuleInfoForUnwind); info->eh_frame_hdr_addr = 0; info->eh_frame_addr = 0; info->eh_frame_size = 0; - info->seg0_addr = reinterpret_cast(mbi.BaseAddress); - info->seg0_size = mbi.RegionSize; + info->seg0_addr = begin; + info->seg0_size = size; + std::strncpy(info->name, name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; +} + +bool fillCachedImageRegion(std::uint64_t addr, std::uintptr_t image, ModuleInfoForUnwind* info) { + std::lock_guard lock(gRegionsMutex); + auto found = gRegions.upper_bound(addr); + if (found == gRegions.begin()) return false; + --found; + const auto& region = found->second; + if (addr >= region.end) return false; + DWORD stamp = 0; + DWORD size = 0; + if (region.image != image || !imageIdentity(image, stamp, size) || stamp != region.stamp || size != region.size) { + gRegions.erase(found); + return false; + } + constexpr std::uint64_t page = 4096; + const std::uint64_t begin = addr & ~(page - 1); + writeUnwindRegion(begin, region.end - begin, region.name, info); + return true; +} +#endif + +bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { +#ifdef _WIN32 + const auto image = loaderImage(addr); + if (image != 0 && fillCachedImageRegion(addr, image, info)) return true; + MEMORY_BASIC_INFORMATION mbi{}; + if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) { + return false; + } char path[4096] = {}; DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); path[len] = '\0'; - std::strncpy(info->name, path, sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; + const auto begin = reinterpret_cast(mbi.BaseAddress); + writeUnwindRegion(begin, mbi.RegionSize, path, info); + ImageRegion region; + if (mbi.Type == MEM_IMAGE && image != 0 && imageIdentity(image, region.stamp, region.size) && begin >= image && begin + mbi.RegionSize <= image + region.size) { + region.end = begin + mbi.RegionSize; + region.image = image; + region.name = path; + std::lock_guard lock(gRegionsMutex); + gRegions.insert_or_assign(begin, std::move(region)); + } return true; #else const auto mapping = HostMapping::Find(addr, "sceSysmoduleGetModuleInfoForUnwind"); @@ -122,7 +186,6 @@ int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); - // throw std::runtime_error(std::string("sceSysmoduleIsLoaded: unknown id ") + std::to_string(id)); return 0; } std::lock_guard lock(gMutex); @@ -139,7 +202,6 @@ int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id) { } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); - // throw std::runtime_error(std::string("sceSysmoduleLoadModule: unknown id ") + std::to_string(id)); return 0; } std::lock_guard lock(gMutex); diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index ec9799ca72..22aad137a6 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -103,10 +103,10 @@ int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gesture_handle, con } int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; (void)index; (void)event; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index d7ff11174c..78aa978dad 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -27,6 +27,7 @@ if(BUILD_TESTING) $ $ $ $ $ $ ${CMAKE_CXX_COMPILER}) set_tests_properties(loadexec_arguments PROPERTIES TIMEOUT 30) + add_asan_test_options(loadexec_arguments verify_asan_link_order=0) endif() add_test_executable(guest_system_exit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemExit.cpp) target_include_directories(guest_system_exit_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -34,22 +35,34 @@ if(BUILD_TESTING) configure_windows_unwind(guest_system_exit_tests) add_test(NAME guest_system_exit_parameters COMMAND guest_system_exit_tests) add_test(NAME guest_system_exit COMMAND guest_system_exit_tests --exit) + add_test(NAME guest_requested_exit COMMAND guest_system_exit_tests --requested-exit-driver) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) - set_tests_properties(guest_system_exit PROPERTIES + add_test(NAME guest_exit_finalizers_before_shutdown COMMAND guest_system_exit_tests --exit-order-driver) + set_tests_properties(guest_system_exit guest_requested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown completed" FAIL_REGULAR_EXPRESSION "Guest exit handler ran|Guest thread resumed during exit") + set_tests_properties(guest_requested_exit PROPERTIES TIMEOUT 10) set_tests_properties(guest_quick_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") set_tests_properties(guest_system_relaunch PROPERTIES PASS_REGULAR_EXPRESSION "Guest relaunch completed" TIMEOUT 30) set_tests_properties(guest_nested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest nested exit completed" TIMEOUT 30) + set_tests_properties(guest_exit_finalizers_before_shutdown PROPERTIES + PASS_REGULAR_EXPRESSION "Guest finalizers ran before shutdown" TIMEOUT 30) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_system_load_exec_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLoadExecStandardError.cpp) + target_include_directories(guest_system_load_exec_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_load_exec_standard_error_tests PRIVATE libSceSystemService libkernel libc) + add_test(NAME guest_system_load_exec_standard_error COMMAND guest_system_load_exec_standard_error_tests) + set_tests_properties(guest_system_load_exec_standard_error PROPERTIES TIMEOUT 20) + endif() if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) @@ -70,6 +83,15 @@ if(BUILD_TESTING) set_tests_properties(guest_system_service_language PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_system_service_time_zone_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceTimeZone.cpp) + target_include_directories(guest_system_service_time_zone_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_time_zone_tests PRIVATE libSceSystemService libc) + configure_windows_unwind(guest_system_service_time_zone_tests) + add_test(NAME guest_system_service_time_zone COMMAND guest_system_service_time_zone_tests) + if(WIN32) + set_tests_properties(guest_system_service_time_zone PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_kill_app_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemKillApp.cpp) target_include_directories(guest_system_kill_app_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_kill_app_tests PRIVATE libSceSystemService libc) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 6b5bb3bb44..6480b49dfc 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,9 +1,11 @@ #include +#include #include #include #include #include #include +#include #include #include #include @@ -40,6 +42,34 @@ int ConsoleLanguage() { return language; } +struct ConsoleTimeZone { + int minutes; + bool summer; +}; + +ConsoleTimeZone HostTimeZone() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#if defined(_WIN32) + const bool converted = localtime_s(&local, &now) == 0; +#else + const bool converted = localtime_r(&now, &local) != nullptr; +#endif + if (!converted) throw std::runtime_error("sceSystemServiceParamGetInt: the host local time is unavailable"); + const std::chrono::sys_days date = std::chrono::year{local.tm_year + 1900} / std::chrono::month{static_cast(local.tm_mon + 1)} / std::chrono::day{static_cast(local.tm_mday)}; + const auto localSeconds = date.time_since_epoch() + std::chrono::hours{local.tm_hour} + std::chrono::minutes{local.tm_min} + std::chrono::seconds{local.tm_sec}; + const auto offset = std::chrono::duration_cast(localSeconds - std::chrono::seconds{now}); + const bool summer = local.tm_isdst > 0; +#if defined(_WIN32) + long daylightBias = 0; + _get_dstbias(&daylightBias); + const int summerMinutes = summer ? static_cast(-daylightBias / 60) : 0; +#else + const int summerMinutes = summer ? 60 : 0; +#endif + return {static_cast(offset.count()) - summerMinutes, summer}; +} + constexpr char MainExecutable[] = "/app0/eboot.bin"; constexpr char ApplicationRoot[] = "/app0/"; constexpr char MainExecutableVariable[] = "APS5_EBOOT"; @@ -183,6 +213,9 @@ void StartHostExecutable(HostLaunch& launch) { std::vector argv; for (auto& argument : launch.arguments) argv.push_back(argument.data()); argv.push_back(nullptr); + if (const int host = HostStandardError_nid_no_patch(); host >= 0 && ::dup2(host, 2) != 2) { + throw std::runtime_error("sceSystemServiceLoadExec: cannot give the host standard error back to descriptor 2: " + std::string(std::strerror(errno))); + } ::execv(launch.executable.c_str(), argv.data()); throw std::runtime_error("sceSystemServiceLoadExec: execv failed: " + std::string(std::strerror(errno))); } @@ -207,6 +240,7 @@ int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* argu #if defined(_WIN32) || defined(__linux__) LoadConvertedExecutable_nid_no_patch(std::filesystem::path(launch.executable), arguments); #endif + LibcStartExit_nid_postfix(); LibcRunShutdown_nid_postfix(); std::fflush(nullptr); StartHostExecutable(launch); @@ -267,8 +301,8 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break; case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break; case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; - case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = 0; break; - case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = 0; break; + case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; + case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; case SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL: *value = SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF; break; case SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN: *value = SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS; break; default: *value = 0; break; diff --git a/core/libs/prx/libSceSystemService/LoadExec.cpp b/core/libs/prx/libSceSystemService/LoadExec.cpp index cc359f4b53..0434f1b817 100644 --- a/core/libs/prx/libSceSystemService/LoadExec.cpp +++ b/core/libs/prx/libSceSystemService/LoadExec.cpp @@ -1,4 +1,5 @@ #include "LoadExec.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/Shutdown.hpp" #include #include @@ -124,6 +125,7 @@ Handle Duplicate(HANDLE source, DWORD access) { throw std::system_error(GetLastError(), std::system_category(), "LoadExec: creating the converted executable"); Handle child(process.hProcess, CloseHandle), thread(process.hThread, CloseHandle); try { + LibcStartExit_nid_postfix(); LibcRunShutdown_nid_postfix(); std::fflush(nullptr); if (ResumeThread(thread.get()) == static_cast(-1)) @@ -151,8 +153,11 @@ Handle Duplicate(HANDLE source, DWORD access) { std::vector pointers; for (const auto& argument : arguments) pointers.push_back(const_cast(argument.c_str())); pointers.push_back(nullptr); + LibcStartExit_nid_postfix(); LibcRunShutdown_nid_postfix(); std::fflush(nullptr); + if (const int host = HostStandardError_nid_no_patch(); host >= 0 && dup2(host, 2) != 2) + throw std::system_error(errno, std::generic_category(), "LoadExec: giving the host standard error back to descriptor 2"); std::filesystem::current_path(target.parent_path()); if ((descriptor > 3 && close_range(3, static_cast(descriptor - 1), 0) != 0) || close_range(static_cast(std::max(3, descriptor + 1)), ~0u, 0) != 0) diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index 26fdb475ef..8d394b9fb3 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -311,6 +312,14 @@ int APS5_VABI sceVdecswTrySyncDecodeInput(std::uint64_t handle, InputResult* res return decoder->inputs.empty() ? VdecswErrorInputQueueEmpty : VdecswErrorDecodePending; } +int APS5_VABI sceVdecswSyncDecodeInput(std::uint64_t handle, InputResult* result) { + for (;;) { + const auto status = sceVdecswTrySyncDecodeInput(handle, result); + if (status != VdecswErrorDecodePending) return status; + std::this_thread::yield(); + } +} + int APS5_VABI sceVdecswSetDecodeOutput(std::uint64_t handle, const FrameBuffer* frame) { if (frame == nullptr || frame->thisSize < sizeof(FrameBuffer) || frame->frameBuffer == nullptr) throw std::runtime_error("Vdecsw: invalid decode output"); const auto decoder = findDecoder(handle); @@ -356,6 +365,14 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t handle, OutputInfo* out return 0; } +int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t handle, OutputInfo* output) { + for (;;) { + const auto status = sceVdecswTrySyncDecodeOutput(handle, output); + if (status != VdecswErrorOutputPending) return status; + std::this_thread::yield(); + } +} + int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { if (output == nullptr || first == nullptr || first->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); @@ -377,6 +394,13 @@ int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInf throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); } +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, void* picture) { + (void)output; + (void)picture; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t handle) { const auto decoder = findDecoder(handle); std::lock_guard lock(decoder->mutex); diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e216b43fb4..e2aaff9966 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -84,7 +84,7 @@ if(BUILD_TESTING) endforeach() set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened) + foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened ps4) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() @@ -184,6 +184,17 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_output_mode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_video_out_color_space_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutColorSpace.cpp) + target_include_directories(guest_video_out_color_space_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_color_space_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_color_space_tests) + add_test(NAME guest_video_out_color_space COMMAND guest_video_out_color_space_tests) + set_tests_properties(guest_video_out_color_space PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_color_space PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ef58c55add..7db06b36c4 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -27,4 +27,19 @@ int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, return sceVideoOutColorSettingsSetGamma(settings, gamma); } +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(settings); +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(settings, lumaScale, option); +} + +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutAdjustColorSpaceConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutAdjustColorSpaceConversion(handle, settings); +} + } diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 633ecc5c8e..8fb708400b 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -8,6 +8,8 @@ extern "C" { +int APS5_VABI sceVideoOutRegisterBuffers(int handle, int buffer_index_start, void* const* addresses, int buffer_num, const VideoOutBufferAttribute* attribute); +void APS5_VABI sceVideoOutSetBufferAttribute(VideoOutBufferAttribute* attribute, uint32_t pixel_format, uint32_t tiling_mode, uint32_t aspect_ratio, uint32_t width, uint32_t height, uint32_t pitch_in_pixel); int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_index_start, const VideoOutBuffers* buffers, int buffer_num, const VideoOutBufferAttribute2* attribute, int category, void* option); void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, uint32_t dcc_control, uint64_t dcc_cb_register_clear_color); void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option); diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index 4ecf913417..569e97ee77 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -28,6 +28,13 @@ int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle); int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point); int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma); int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option); int APS5_VABI sceVideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t arg2); int APS5_VABI sceVideoOutVrrUnpegFromFixedRate(int handle); diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index ecf9c559b2..069a00c174 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -107,6 +107,7 @@ struct BufferAttributeGroup { VideoOutBufferAttribute2 attribute{}; int category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED; bool occupied = false; + bool ps4 = false; }; AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, const BufferAttributeGroup& group); @@ -132,6 +133,7 @@ struct VideoOutConfig { int busType = VIDEO_OUT_BUS_TYPE_MAIN; bool opened = false; bool closing = false; + bool ps4 = false; std::exception_ptr failure; std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); int flipRate = 0; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 4a80aa47d4..81722d188e 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -24,11 +24,62 @@ AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, co if (compressed && buffer.metadataAddress == 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " without DCC metadata"); if (compressed && attribute.tiling_mode != 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " with linear tiling"); if (compressed && (attribute.dcc_control & ~VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) != 0) throw std::runtime_error("VideoOut: unsupported DCC control " + hex(attribute.dcc_control) + " (only the block size and independence fields " + hex(VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) + " are known)"); - const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel, compressed ? buffer.metadataAddress : 0, compressed ? attribute.dcc_cb_register_clear_color : 0}; + AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel, compressed ? buffer.metadataAddress : 0, compressed ? attribute.dcc_cb_register_clear_color : 0}; + if (group.ps4) result.alignment = 4096; static_cast(AgcDriverDisplayBufferSize_nid_postfix(result)); return result; } +namespace { + +int CommitBufferGroup(const std::string& function, VideoOutConfig& cfg, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const BufferAttributeGroup& group) { + std::unique_lock lock(cfg.mutex); + cfg.Check(); + if (cfg.closing) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + if (setIndex < 0) { + for (int index = 0; index < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX && setIndex < 0; ++index) + if (!cfg.groups[index].occupied) setIndex = index; + if (setIndex < 0) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_NO_EMPTY_SLOT"); + } + } else if (cfg.groups[setIndex].occupied) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_INVALID_INDEX"); + } + for (int i = 0; i < bufferNum; i++) { + if (cfg.buffers[bufferIndexStart + i].Occupied()) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_SLOT_OCCUPIED"); + } + } + std::array registered{}; + for (int i = 0; i < bufferNum; ++i) { + registered[i] = {setIndex, reinterpret_cast(buffers[i].data), reinterpret_cast(buffers[i].metadata)}; + const auto display = DescribeVideoOutBuffer(registered[i], group); + AgcDriverCheckGuestMemory_nid_postfix(buffers[i].data, AgcDriverDisplayBufferSize_nid_postfix(display), display.alignment); + } + cfg.groups[setIndex] = group; + cfg.width = group.attribute.width; + cfg.height = group.attribute.height; + for (int i = 0; i < bufferNum; i++) { + cfg.buffers[bufferIndexStart + i] = registered[i]; + auto& label = cfg.bufferLabels[bufferIndexStart + i]; + AgcDriverStoreGuestLabel_nid_postfix(&label, 0); + } + if (group.ps4) cfg.ps4 = true; + return setIndex; +} + +uint64_t Ps5PixelFormat(uint32_t pixelFormat) { + if (pixelFormat == 0x80000000u) return 0x8000000000000000ull; + if (pixelFormat == 0x80002200u) return 0x8000000022000000ull; + char text[96]; + std::snprintf(text, sizeof(text), "sceVideoOutRegisterBuffers: unsupported PS4 pixel format 0x%08x", pixelFormat); + throw std::runtime_error(text); +} + +} + extern "C" { int APS5_VABI sceVideoOutGetBufferLabelAddress(int handle, uintptr_t* address) try { @@ -91,42 +142,65 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn } AgcDriverCheckGuestMemory_nid_postfix(attribute, sizeof(*attribute), alignof(VideoOutBufferAttribute2)); AgcDriverCheckGuestMemory_nid_postfix(buffers, static_cast(bufferNum) * sizeof(*buffers), alignof(VideoOutBuffers)); - std::unique_lock lock(cfg->mutex); - cfg->Check(); - if (cfg->closing) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); - } - if (cfg->groups[setIndex].occupied) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX"); - } - for (int i = 0; i < bufferNum; i++) { - if (cfg->buffers[bufferIndexStart + i].Occupied()) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_SLOT_OCCUPIED"); - } - } BufferAttributeGroup group{}; group.attribute = *attribute; group.category = category; group.occupied = true; - std::array registered{}; - for (int i = 0; i < bufferNum; ++i) { - registered[i] = {setIndex, reinterpret_cast(buffers[i].data), reinterpret_cast(buffers[i].metadata)}; - const auto display = DescribeVideoOutBuffer(registered[i], group); - AgcDriverCheckGuestMemory_nid_postfix(buffers[i].data, AgcDriverDisplayBufferSize_nid_postfix(display), 65536); - } - cfg->groups[setIndex] = group; - cfg->width = attribute->width; - cfg->height = attribute->height; - for (int i = 0; i < bufferNum; i++) { - cfg->buffers[bufferIndexStart + i] = registered[i]; - auto& label = cfg->bufferLabels[bufferIndexStart + i]; - AgcDriverStoreGuestLabel_nid_postfix(&label, 0); - } + CommitBufferGroup(__func__, *cfg, setIndex, bufferIndexStart, buffers, bufferNum, group); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } +void APS5_VABI sceVideoOutSetBufferAttribute(VideoOutBufferAttribute* attribute, uint32_t pixelFormat, uint32_t tilingMode, uint32_t aspectRatio, uint32_t width, uint32_t height, uint32_t pitchInPixel) { + if (attribute == nullptr) { + throw std::runtime_error("sceVideoOutSetBufferAttribute: null attribute"); + } + std::memset(attribute, 0, sizeof(VideoOutBufferAttribute)); + attribute->pixel_format = pixelFormat; + attribute->tiling_mode = tilingMode; + attribute->aspect_ratio = aspectRatio; + attribute->width = width; + attribute->height = height; + attribute->pitch_in_pixel = pitchInPixel; +} + +int APS5_VABI sceVideoOutRegisterBuffers(int handle, int bufferIndexStart, void* const* addresses, int bufferNum, const VideoOutBufferAttribute* attribute) try { + auto cfg = VideoOutDriver::Get().GetConfig(handle); + if (cfg == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + if (addresses == nullptr || attribute == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + if (bufferIndexStart < 0 || bufferIndexStart >= VIDEO_OUT_BUFFER_NUM_MAX || bufferNum < 1 || bufferNum > VIDEO_OUT_BUFFER_NUM_MAX || bufferIndexStart + bufferNum > VIDEO_OUT_BUFFER_NUM_MAX) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); + } + AgcDriverCheckGuestMemory_nid_postfix(attribute, sizeof(*attribute), alignof(VideoOutBufferAttribute)); + AgcDriverCheckGuestMemory_nid_postfix(addresses, static_cast(bufferNum) * sizeof(*addresses), alignof(void*)); + if (attribute->option != 0 || attribute->reserved0 != 0 || attribute->reserved1 != 0) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION"); + } + if (attribute->tiling_mode != 1) { + throw std::runtime_error(std::string(__func__) + ": PS4 tiled display buffers are not implemented"); + } + BufferAttributeGroup group{}; + group.attribute.pixel_format = Ps5PixelFormat(attribute->pixel_format); + group.attribute.tiling_mode = attribute->tiling_mode; + group.attribute.aspect_ratio = attribute->aspect_ratio; + group.attribute.width = attribute->width; + group.attribute.height = attribute->height; + group.attribute.pitch_in_pixel = attribute->pitch_in_pixel; + group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED; + group.occupied = true; + group.ps4 = true; + std::array buffers{}; + for (int i = 0; i < bufferNum; ++i) buffers[i].data = addresses[i]; + return CommitBufferGroup(__func__, *cfg, -1, bufferIndexStart, buffers.data(), bufferNum, group); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { @@ -152,7 +226,7 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) { if (cfg->buffers[i].groupIndex != setIndex) continue; const auto display = DescribeVideoOutBuffer(cfg->buffers[i], group); - AgcDriverCheckGuestMemory_nid_postfix(reinterpret_cast(display.address), AgcDriverDisplayBufferSize_nid_postfix(display), 65536); + AgcDriverCheckGuestMemory_nid_postfix(reinterpret_cast(display.address), AgcDriverDisplayBufferSize_nid_postfix(display), display.alignment); } cfg->groups[setIndex] = group; return 0; diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index d8cf0e3d7c..9b58c374de 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -104,6 +104,10 @@ void Pad::KeyboardMouse::SetMouseMode(bool enabled) { mouse.Reset(); } +void Pad::KeyboardMouse::SetTitleReadsMouse(bool reads) { + titleReadsMouse = reads; +} + void Pad::KeyboardMouse::Release() { std::fill(pressed.begin(), pressed.end(), false); std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), Clock::time_point{}); @@ -118,10 +122,11 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock return requests; } if (event.type == SDL_MOUSEMOTION) { - if (mouseMode) mouse.Move(event.motion.xrel, event.motion.yrel); + if (mouseMode && !titleReadsMouse) mouse.Move(event.motion.xrel, event.motion.yrel); return requests; } if (event.type == SDL_MOUSEWHEEL) { + if (titleReadsMouse) return requests; int direction = (event.wheel.y > 0) - (event.wheel.y < 0); if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; if (direction == 0) return requests; @@ -138,6 +143,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock const bool mouseButton = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; if (!keyboard && !mouseButton) return requests; if (keyboard && event.key.repeat != 0) return requests; + if (event.type == SDL_MOUSEBUTTONDOWN && titleReadsMouse) return requests; const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { const auto& binding = configuration.bindings[index]; diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 4286633290..f1e79af366 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -82,6 +82,10 @@ bool MouseInput::CaptureWanted(bool allowed) const { } void MouseInput::UpdateCapture(bool allowed) { + if (!allowed) { + captured = false; + return; + } const bool wanted = CaptureWanted(allowed); if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 5dc9e8e768..79af66728a 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -136,7 +136,13 @@ int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) t } std::unique_lock lock(cfg->mutex); cfg->Check(); - *status = cfg->flipStatus; + if (cfg->ps4) { + const auto& current = cfg->flipStatus; + VideoOutFlipStatusPs4 legacy{current.count, current.processTime, 0, current.flipArg, 0, 0, current.gcQueueNum, current.flipPendingNum, current.currentBuffer, 0}; + *reinterpret_cast(status) = legacy; + } else { + *status = cfg->flipStatus; + } return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -152,7 +158,13 @@ int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* statu } std::unique_lock lock(cfg->mutex); cfg->Check(); - *status = cfg->vblankStatus; + if (cfg->ps4) { + const auto& current = cfg->vblankStatus; + VideoOutVblankStatusPs4 legacy{current.count, current.processTime, 0, current.flags}; + *reinterpret_cast(status) = legacy; + } else { + *status = cfg->vblankStatus; + } return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -327,6 +339,48 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings) { + if (settings == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + std::memset(settings, 0, sizeof(VideoOutColorSpaceConversionSettings)); + return 0; +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option) { + if (settings == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + (void)lumaScale; + (void)option; + return 0; +} + +int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings) try { + if (settings == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + auto cfg = VideoOutDriver::Get().GetConfig(handle); + if (cfg == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option) try { + auto cfg = VideoOutDriver::Get().GetConfig(handle); + if (cfg == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + (void)space; + (void)option; + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutVrrUnpegFromFixedRate(int handle) try { if (VideoOutDriver::Get().GetConfig(handle) == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 06352b8775..25ccd5af4a 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -9,6 +9,7 @@ #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" @@ -224,6 +225,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; if (!input || ignoreInput) return; if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; + keyboardMouse.SetTitleReadsMouse(MouseIsOpen_nid_no_patch()); const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index f38acd2925..0f991c0c3c 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -124,6 +124,75 @@ void testLifetime(bool reopen) { LibcRunShutdown_nid_postfix(); } +void testPs4Buffers() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + std::vector allocation(2 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBufferAttribute attribute{}; + sceVideoOutSetBufferAttribute(&attribute, 0x80000000u, 1, 0, 64, 64, 64); + check(attribute.pixel_format == 0x80000000u && attribute.tiling_mode == 1 && attribute.aspect_ratio == 0 && attribute.width == 64 && attribute.height == 64 && attribute.pitch_in_pixel == 64 && attribute.option == 0 && attribute.reserved0 == 0 && attribute.reserved1 == 0, "PS4 buffer attribute fields are wrong"); + void* const pair[2] = {storage.data(), storage.data() + 20480}; + VideoOutBuffers second{pair[1], nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 ps5{}; + ps5.width = 64; + ps5.height = 64; + ps5.tiling_mode = 1; + ps5.pitch_in_pixel = 64; + ps5.pixel_format = 0x8000000000000000ull; + check(expectFailure([&] { sceVideoOutRegisterBuffers2(handle, 3, 5, &second, 1, &ps5, 0, nullptr); }).find("64 KiB alignment") != std::string::npos, "a PS5 buffer was accepted at 4 KiB alignment"); + check(sceVideoOutRegisterBuffers(handle, 0, pair, 2, &attribute) == 0, "the first PS4 set index is not 0"); + void* const third[1] = {storage.data() + 49152}; + check(sceVideoOutRegisterBuffers(handle, 2, third, 1, &attribute) == 1, "the second PS4 set index is not 1"); + { + const auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::lock_guard lock(cfg->mutex); + check(cfg->groups[0].attribute.pixel_format == 0x8000000000000000ull && cfg->buffers[1].dataAddress == reinterpret_cast(pair[1]) && cfg->buffers[2].groupIndex == 1, "PS4 buffers were registered wrongly"); + } + VideoOutBufferAttribute2 tiledChange{}; + tiledChange.width = 64; + tiledChange.height = 64; + tiledChange.pixel_format = 0x8000000000000000ull; + check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &tiledChange, nullptr); }).find("only supported for linear buffers") != std::string::npos, "a PS4 buffer set was changed to tiled"); + void* const odd[1] = {storage.data() + 65536 + 256}; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, odd, 1, &attribute); }).find("4 KiB alignment") != std::string::npos, "a PS4 buffer was accepted below 4 KiB alignment"); + void* const fourth[1] = {storage.data() + 65536}; + auto tiled = attribute; + tiled.tiling_mode = 0; + tiled.pitch_in_pixel = 0; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, fourth, 1, &tiled); }).find("tiled display buffers are not implemented") != std::string::npos, "a PS4 tiled buffer was accepted"); + auto tenBit = attribute; + tenBit.pixel_format = 0x88060000u; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, fourth, 1, &tenBit); }).find("unsupported PS4 pixel format 0x88060000") != std::string::npos, "an unknown PS4 pixel format was accepted"); + auto withOption = attribute; + withOption.option = 1; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, fourth, 1, &withOption); }).find("VIDEO_OUT_ERROR_INVALID_OPTION") != std::string::npos, "a PS4 buffer option was accepted"); + std::array raw{}; + raw.fill(0xaa); + check(sceVideoOutGetFlipStatus(handle, reinterpret_cast(raw.data())) == 0, "the PS4 flip status failed"); + check(std::all_of(raw.begin() + sizeof(VideoOutFlipStatusPs4), raw.end(), [](std::uint8_t value) { return value == 0xaa; }), "the PS4 flip status overflowed its 64 bytes"); + VideoOutFlipStatusPs4 flip; + std::memcpy(&flip, raw.data(), sizeof(flip)); + check(flip.count == 0 && flip.flipArg == -1 && flip.currentBuffer == -1 && flip.flipPendingNum == 0, "the PS4 flip status fields are wrong"); + raw.fill(0xaa); + check(sceVideoOutGetVblankStatus(handle, reinterpret_cast(raw.data())) == 0, "the PS4 vblank status failed"); + check(std::all_of(raw.begin() + sizeof(VideoOutVblankStatusPs4), raw.end(), [](std::uint8_t value) { return value == 0xaa; }), "the PS4 vblank status overflowed its 32 bytes"); + { + const auto cfg = VideoOutDriver::Get().GetConfig(handle); + check(sceVideoOutSubmitFlip(handle, 1, 1, 7) == 0, "the PS4 flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == 1; }), "the PS4 flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipArg == 7 && cfg->flipStatus.currentBuffer == 1, "the PS4 flip status is wrong"); + } + sceVideoOutClose(handle); + const int ps5Handle = sceVideoOutOpen(255, 0, 0, nullptr); + raw.fill(0xaa); + check(sceVideoOutGetFlipStatus(ps5Handle, reinterpret_cast(raw.data())) == 0, "the PS5 flip status failed"); + check(std::all_of(raw.begin() + sizeof(VideoOutFlipStatusPs4), raw.end(), [](std::uint8_t value) { return value == 0; }), "the PS5 flip status did not fill its 128 bytes"); + sceVideoOutClose(ps5Handle); + LibcRunShutdown_nid_postfix(); +} + void testDeferredBuffer() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -905,6 +974,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); + else if (argc == 2 && std::string(argv[1]) == "ps4") testPs4Buffers(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); return 0; diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index 93715508cb..fa9b6f7e3a 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -286,6 +286,50 @@ void testToggleRequests() { check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); } + +void testTitleReadsMouse() { + const auto square = static_cast(Pad::PadButton::Square); + const auto r2 = static_cast(Pad::PadButton::R2); + const auto up = static_cast(Pad::PadButton::Up); + const auto down = static_cast(Pad::PadButton::Down); + const auto wheelDirections = up | down; + auto now = Pad::KeyboardMouse::Clock::now(); + Pad::KeyboardMouse keyboardMouse(defaults()); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, true), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & square) != 0, "a left click presses Square while the title does not read the mouse"); + keyboardMouse.SetTitleReadsMouse(true); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, false), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & square) == 0, "releasing a button pressed before the title read the mouse left Square held"); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, true), now); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_RIGHT, true), now); + const auto clicked = keyboardMouse.Compose(PadInputState{}); + check((clicked.buttons & (square | r2)) == 0 && clicked.analogButtonsR2 == 0, "a click while the title reads the mouse pressed a pad button"); + keyboardMouse.HandleEvent(PadSimulation::WheelEvent(1), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & wheelDirections) == 0, "wheel up pressed a pad direction while the title reads the mouse"); + keyboardMouse.HandleEvent(PadSimulation::WheelEvent(-1), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & wheelDirections) == 0, "wheel down pressed a pad direction while the title reads the mouse"); + check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "a middle click while the title reads the mouse asked to toggle mouse mode"); + keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_W, true), now); + check(keyboardMouse.Compose(PadInputState{}).sticks[1] == 0, "keys stopped reaching the pad while the title reads the mouse"); + keyboardMouse.SetMouseMode(true); + keyboardMouse.Advance(now); + for (int step = 1; step <= 40; ++step) { + now += std::chrono::milliseconds(8); + keyboardMouse.HandleEvent(PadSimulation::MotionEvent(200, 0), now); + keyboardMouse.Advance(now); + } + check(keyboardMouse.Compose(PadInputState{}).sticks[2] == 128, "motion tilted the right stick while the title reads the mouse"); + keyboardMouse.SetTitleReadsMouse(false); + keyboardMouse.HandleEvent(PadSimulation::WheelEvent(1), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & up) != 0, "wheel bindings did not resume after the title stopped reading the mouse"); + for (int step = 1; step <= 40; ++step) { + now += std::chrono::milliseconds(8); + keyboardMouse.HandleEvent(PadSimulation::MotionEvent(200, 0), now); + keyboardMouse.Advance(now); + } + check(keyboardMouse.Compose(PadInputState{}).sticks[2] > 128, "motion in mouse mode no longer tilts the stick once the title stops reading the mouse"); +} + } int main() { @@ -305,6 +349,7 @@ int main() { {"wheel and keys on focus loss", testWheelFocusLoss}, {"focus loss releases the mouse", testFocusLossReleasesMouse}, {"toggle requests", testToggleRequests}, + {"title reads the mouse", testTitleReadsMouse}, }; for (const auto& [name, test] : tests) { test(); diff --git a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp index 5d740a05e4..759c9708e7 100644 --- a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp @@ -67,7 +67,14 @@ void CaptureApplied(MouseInput& input) { input.UpdateCapture(true); const bool applied = SDL_GetRelativeMouseMode() == SDL_TRUE; input.UpdateCapture(false); - Require(SDL_GetRelativeMouseMode() == SDL_FALSE, "the capture was not released"); + if (applied) { + Require(SDL_GetRelativeMouseMode() == SDL_TRUE, "mouse-to-stick mode lost relative mouse mode"); + Require(SDL_SetRelativeMouseMode(SDL_FALSE) == 0, "mouse-to-stick mode could not release relative mode"); + input.UpdateCapture(true); + Require(SDL_GetRelativeMouseMode() == SDL_TRUE, "guest mouse capture was not restored after mouse-to-stick mode"); + input.UpdateCapture(false); + Require(SDL_SetRelativeMouseMode(SDL_FALSE) == 0, "relative mouse mode could not be reset"); + } std::printf("mouse input: SDL %s relative mode under the %s driver\n", applied ? "applied" : "refused", SDL_GetCurrentVideoDriver()); SDL_Quit(); } diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 971db75652..ea5ca36c4e 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -303,6 +303,7 @@ struct Decoder { std::int64_t nextKey = 0; std::deque ready; std::vector annexB; + std::vector parameterSets; bool flushing = false; std::array vp9References{}; Vp9PictureInfo vp9Colors{}; @@ -389,6 +390,16 @@ const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* return &decoder.annexB; } +bool CarriesAvcSlice(const std::vector& unit) { + for (std::size_t index = 0; index + 3 < unit.size(); ++index) { + if (unit[index] != 0 || unit[index + 1] != 0 || unit[index + 2] != 1) continue; + const auto type = unit[index + 3] & 0x1fu; + if (type >= 1 && type <= 5) return true; + index += 2; + } + return false; +} + void Receive(Decoder& decoder) { for (;;) { FramePointer frame(av_frame_alloc()); @@ -544,7 +555,10 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa if (Trace() && decoder.decoded < 3) std::fprintf(stderr, "[videodec2] Decode input=%llu frame=%llu output=%llu au=%llu pts=%llu size=%ux%u fb=%p/%llu\n", static_cast(input->thisSize), static_cast(frame ? frame->thisSize : 0), static_cast(output->thisSize), static_cast(input->auSize), static_cast(input->ptsData), decoder.width, decoder.height, frame ? frame->frameBuffer : nullptr, static_cast(frame ? frame->frameBufferSize : 0)); ++decoder.decoded; const Picture picture{input->ptsData, input->dtsData, input->attachedData}; - if (decoder.flushing) throw std::runtime_error("Videodec2: reset required after flushing"); + if (decoder.flushing) { + avcodec_flush_buffers(decoder.context); + decoder.flushing = false; + } if (!input->auData || input->auSize == 0 || input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); if (decoder.codec == CodecVp9) { auto references = decoder.vp9References; @@ -574,12 +588,20 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa return 0; } const auto* unit = ToAnnexB(decoder, input->auData, static_cast(input->auSize)); + if (decoder.codec == CodecAvc && !CarriesAvcSlice(*unit)) { + decoder.parameterSets.insert(decoder.parameterSets.end(), unit->begin(), unit->end()); + EmitDecoded(handle, decoder, frame, output); + return 0; + } AVPacket* packet = av_packet_alloc(); if (!packet) throw std::runtime_error("Videodec2: cannot allocate packet"); const auto freePacket = [](AVPacket* value) { av_packet_free(&value); }; const std::unique_ptr packetOwner(packet, freePacket); - if (av_new_packet(packet, static_cast(unit->size())) < 0) throw std::runtime_error("Videodec2: cannot allocate packet data"); - std::memcpy(packet->data, unit->data(), unit->size()); + if (decoder.parameterSets.size() + unit->size() > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); + if (av_new_packet(packet, static_cast(decoder.parameterSets.size() + unit->size())) < 0) throw std::runtime_error("Videodec2: cannot allocate packet data"); + std::memcpy(packet->data, decoder.parameterSets.data(), decoder.parameterSets.size()); + std::memcpy(packet->data + decoder.parameterSets.size(), unit->data(), unit->size()); + decoder.parameterSets.clear(); packet->pts = decoder.nextKey; decoder.inputs.emplace(decoder.nextKey++, picture); Send(decoder, packet); @@ -616,6 +638,7 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { avcodec_flush_buffers(decoder.context); decoder.ready.clear(); decoder.inputs.clear(); + decoder.parameterSets.clear(); decoder.flushing = false; return 0; } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index f3234417f7..d5a746c74a 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -19,6 +20,7 @@ constexpr std::int32_t PortOutVoice = 4; constexpr std::int32_t PortOutDevice = 5; constexpr std::int32_t PortStateReady = 1; constexpr std::uint32_t VoiceFrameMs = 20; +constexpr std::int32_t PortAttrVoiceActivity = 1001; struct Port { std::int32_t type; @@ -179,11 +181,14 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { } int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)value; - (void)size; auto& voice = State(); std::lock_guard lock(voice.mutex); RequirePort(voice, port_id, __func__); + if (attr == PortAttrVoiceActivity) { + if (value == nullptr || size != 1) APS5_INVALID_ARG_EX; + std::memset(value, 0, 1); + return 0; + } Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); } @@ -220,8 +225,16 @@ int APS5_VABI sceVoiceGetMuteFlag(uint32_t port_id, bool* muted) { return 0; } -int APS5_VABI sceVoiceGetResourceInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo* info) { + if (info == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + info->max_voice_input_ports = 7; + info->max_voice_output_ports = 7; + info->max_device_input_ports = 1; + info->max_device_output_ports = 1; + info->max_ports = 64; return 0; } diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index e8213885a5..d8dc8fe238 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,6 +1,11 @@ #include #include +#include +#include +#include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -13,56 +18,117 @@ constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast(0x804E0901 constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast(0x804E0902); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); -std::mutex g_mutex; -bool g_initialized = false; +using Endpoint = std::map>; + +struct Connection { + std::int32_t local; + std::int32_t remote; +}; + +struct QoS { + std::mutex mutex; + bool initialized = false; + std::int32_t nextId = 1; + std::map locals; + std::map remotes; + std::map connections; +}; + +QoS& State() { + static QoS qos; + return qos; +} + +std::int32_t NewId(QoS& qos) { + const std::int32_t id = qos.nextId; + qos.nextId = qos.nextId == std::numeric_limits::max() ? 1 : qos.nextId + 1; + return id; +} + +int CreateEndpoint(std::int32_t* id, std::map& endpoints, QoS& qos) { + if (!id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *id = NewId(qos); + endpoints.emplace(*id, Endpoint{}); + return 0; +} + +int DeleteEndpoint(std::int32_t id, std::map& endpoints) { + return endpoints.erase(id) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; +} + +int SetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, const void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (size > 0) { + const auto* bytes = static_cast(value); + endpoint->second[attribute].assign(bytes, bytes + size); + } else { + endpoint->second[attribute].clear(); + } + return 0; +} + +template +int Locked(Body&& body) { + auto& qos = State(); + std::lock_guard lock(qos.mutex); + if (!qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + return body(qos); +} } extern "C" { -int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_type) { - std::lock_guard lock(g_mutex); - if (g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; - if (!mem_block || mem_size != VOICE_QOS_MEMORY_SIZE || (app_type != VOICE_QOS_APP_TYPE_GAME && app_type != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - g_initialized = true; +int APS5_VABI sceVoiceQoSInit(void* memBlock, uint32_t memSize, int32_t appType) { + auto& qos = State(); + std::lock_guard lock(qos.mutex); + if (qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; + if (!memBlock || memSize != VOICE_QOS_MEMORY_SIZE || (appType != VOICE_QOS_APP_TYPE_GAME && appType != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + qos.initialized = true; return 0; } int APS5_VABI sceVoiceQoSEnd() { - std::lock_guard lock(g_mutex); - if (!g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; - g_initialized = false; - return 0; + return Locked([](QoS& qos) { + qos.connections.clear(); + qos.locals.clear(); + qos.remotes.clear(); + qos.initialized = false; + return 0; + }); } -int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(int32_t* localId, int32_t userId, int32_t deviceInId, int32_t deviceOutId) { + (void)userId; + (void)deviceInId; + (void)deviceOutId; + return Locked([&](QoS& qos) { return CreateEndpoint(localId, qos.locals, qos); }); } -int APS5_VABI sceVoiceQoSDeleteLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(int32_t localId) { + return Locked([&](QoS& qos) { return DeleteEndpoint(localId, qos.locals); }); } -int APS5_VABI sceVoiceQoSCreateRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(int32_t* remoteId) { + return Locked([&](QoS& qos) { return CreateEndpoint(remoteId, qos.remotes, qos); }); } -int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(int32_t remoteId) { + return Locked([&](QoS& qos) { return DeleteEndpoint(remoteId, qos.remotes); }); } -int APS5_VABI sceVoiceQoSConnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { + return Locked([&](QoS& qos) { + if (!connectionId || !qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *connectionId = NewId(qos); + qos.connections.emplace(*connectionId, Connection{localId, remoteId}); + return 0; + }); } -int APS5_VABI sceVoiceQoSDisconnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDisconnect(int32_t connectionId) { + return Locked([&](QoS& qos) { return qos.connections.erase(connectionId) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; }); } int APS5_VABI sceVoiceQoSSetConnectionAttribute() { @@ -70,14 +136,12 @@ int APS5_VABI sceVoiceQoSSetConnectionAttribute() { return 0; } -int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(int32_t localId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) { return SetAttribute(qos.locals, localId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); } int APS5_VABI sceVoiceQoSReadPacket() { diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 42b258980d..9e3639df41 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -10,6 +10,16 @@ int APS5_VABI sceVrTracker2Finalize() { return 0; } +int APS5_VABI sceVrTracker2GetCoordinateSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2ResetLocalCoordinate() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index cf61a32659..01bf0684d8 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -72,8 +72,12 @@ int APS5_VABI sceWebBrowserDialogSetCookie(void) { return 0; } -int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(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; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 1fe621e9eb..7344519b44 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,7 +1,33 @@ +if(BUILD_TESTING) + add_test_executable(guest_binary_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestBinaryWord.cpp) + target_include_directories(guest_binary_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_binary_word_tests PRIVATE libc) + configure_windows_unwind(guest_binary_word_tests) + add_test(NAME guest_binary_word COMMAND guest_binary_word_tests guest_binary_word.tmp) + set_tests_properties(guest_binary_word PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_binary_word PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_safe_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSafeErrors.cpp) + target_include_directories(guest_safe_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_safe_error_tests PRIVATE libc) + configure_windows_unwind(guest_safe_error_tests) + add_test(NAME guest_safe_errors COMMAND guest_safe_error_tests) + set_tests_properties(guest_safe_errors PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_safe_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AppArchive.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp @@ -16,8 +42,10 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HostErrno.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp @@ -52,15 +80,50 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) +target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) + target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) +target_link_libraries(libc PRIVATE zarchive) + if(WIN32) target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi) endif() if(BUILD_TESTING) + if(UNIX) + add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp) + target_include_directories(guest_fopen_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fopen_errors_tests PRIVATE libc) + add_test(NAME guest_fopen_errors COMMAND guest_fopen_errors_tests) + set_tests_properties(guest_fopen_errors PROPERTIES TIMEOUT 20) + endif() + + add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) + target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_allocating_strings_tests PRIVATE libc) + configure_windows_unwind(guest_allocating_strings_tests) + add_test(NAME guest_allocating_strings COMMAND guest_allocating_strings_tests) + if(WIN32) + set_tests_properties(guest_allocating_strings PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppArchive.cpp) + target_include_directories(guest_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_archive_tests PRIVATE libc zarchive) + configure_windows_unwind(guest_app_archive_tests) + foreach(mode present absent damaged invalid) + add_test(NAME guest_app_archive_${mode} COMMAND guest_app_archive_tests ${mode}) + set_tests_properties(guest_app_archive_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_archive_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_array_callbacks_tests ${LIBS_INCLUDE_DIR}/tests/GuestArrayCallbacks.cpp) target_include_directories(guest_array_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_array_callbacks_tests PRIVATE libc) @@ -138,6 +201,15 @@ if(BUILD_TESTING) set_tests_properties(file_position PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_fopen_long_host_path_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenLongHostPath.cpp) + target_include_directories(guest_fopen_long_host_path_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fopen_long_host_path_tests PRIVATE libc) + configure_windows_unwind(guest_fopen_long_host_path_tests) + add_test(NAME guest_fopen_long_host_path COMMAND guest_fopen_long_host_path_tests) + set_tests_properties(guest_fopen_long_host_path PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_wide_formatting_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideFormatting.cpp) target_include_directories(guest_wide_formatting_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_formatting_tests PRIVATE libc) @@ -390,8 +462,9 @@ if(BUILD_TESTING) target_link_libraries(guest_standard_stream_tests PRIVATE libc) configure_windows_unwind(guest_standard_stream_tests) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) + add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) if(WIN32) - set_tests_properties(guest_standard_streams PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) @@ -616,6 +689,11 @@ if(BUILD_TESTING) add_test_executable(cpu_topology_tests ${LIBS_INCLUDE_DIR}/tests/CpuTopology.cpp) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) + add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) + target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_backtrace_tests PRIVATE libc) + set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) + add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -715,6 +793,7 @@ if(MINGW) target_link_libraries(guest_message_destructor_abi_tests PRIVATE libc) configure_windows_unwind(guest_message_destructor_abi_tests) foreach(mode IN ITEMS control decoy zero release regex-decoy regex-zero + runtime-decoy runtime-zero doraise-decoy doraise-zero unshared-decoy unshared-zero regex-unshared-decoy regex-unshared-zero) add_test(NAME guest_message_destructor_${mode} COMMAND guest_message_destructor_abi_tests ${mode}) set_tests_properties(guest_message_destructor_${mode} PROPERTIES TIMEOUT 10 @@ -748,5 +827,15 @@ if(MINGW) set_tests_properties(windows_backtrace_report PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" PASS_REGULAR_EXPRESSION "aborting[^~]*SIGABRT. on thread[^~]*stack return address candidates:[^~]*rsp[+]0x") + add_test(NAME windows_backtrace_failure_report COMMAND windows_backtrace_report_tests report) + set_tests_properties(windows_backtrace_failure_report PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" + PASS_REGULAR_EXPRESSION "aborting[^~]*SIGABRT. on thread[^~]*all threads:[^~]*kept failure report ran" + FAIL_REGULAR_EXPRESSION "dropped failure report ran;kept failure report ran[^~]*kept failure report ran") + add_test(NAME windows_backtrace_failure_report_once COMMAND windows_backtrace_report_tests report-once) + set_tests_properties(windows_backtrace_failure_report_once PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" + PASS_REGULAR_EXPRESSION "kept failure report ran[^~]*aborting[^~]*SIGABRT. on thread[^~]*all threads:" + FAIL_REGULAR_EXPRESSION "kept failure report ran[^~]*kept failure report ran") endif() endif() diff --git a/core/libs/prx/libc/include/AppArchive.hpp b/core/libs/prx/libc/include/AppArchive.hpp new file mode 100644 index 0000000000..13d25f34c3 --- /dev/null +++ b/core/libs/prx/libc/include/AppArchive.hpp @@ -0,0 +1,48 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_APPARCHIVE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_APPARCHIVE_HPP + +#include +#include +#include +#include +#include + +struct AppArchiveNode { + std::uint32_t Handle; + bool Directory; + std::uint64_t Size; +}; + +struct AppArchiveBuffer { + void* Base; + std::size_t Length; +}; + +struct AppArchiveDirectoryEntry { + std::string Name; + bool Directory; +}; + +extern "C" { + +bool AppArchiveLookup_nid_no_patch(const std::filesystem::path& hostPath, AppArchiveNode* node); +std::uint64_t AppArchiveRead_nid_no_patch(std::uint32_t handle, std::uint64_t offset, void* buffer, std::uint64_t length); +std::vector AppArchiveList_nid_no_patch(std::uint32_t handle); +void AppArchiveReady_nid_no_patch(); +void AppArchiveRefuseOnly_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath, const char* operation); +void AppArchiveRefuseMergedListing_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath); +void AppArchiveRefuseDescriptor_nid_no_patch(int descriptor, const char* operation); +std::filesystem::path AppArchivePath_nid_no_patch(); +bool AppArchiveOnly_nid_no_patch(const std::filesystem::path& hostPath); +bool AppArchiveHasDirectory_nid_no_patch(const std::filesystem::path& hostPath); + +void AppArchiveAttach_nid_no_patch(int descriptor, const AppArchiveNode& node); +void AppArchiveShare_nid_no_patch(int from, int to); +void AppArchiveDetach_nid_no_patch(int descriptor); +bool AppArchiveDescriptor_nid_no_patch(int descriptor, AppArchiveNode* node, std::uint64_t* position); +bool AppArchiveDescriptorRead_nid_no_patch(int descriptor, const AppArchiveBuffer* buffers, int count, const std::int64_t* offset, std::uint64_t* read); +bool AppArchiveDescriptorSeek_nid_no_patch(int descriptor, std::int64_t offset, int whence, std::int64_t* position); + +} + +#endif diff --git a/core/libs/prx/libc/include/CrashText.hpp b/core/libs/prx/libc/include/CrashText.hpp new file mode 100644 index 0000000000..675cc7c096 --- /dev/null +++ b/core/libs/prx/libc/include/CrashText.hpp @@ -0,0 +1,59 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP + +#include +#include + +namespace crash_text { + +constexpr std::size_t MaximumCharacters = 512; +constexpr std::size_t MinimumCharacters = 4; +constexpr std::size_t MaximumBytes = 2 * (MaximumCharacters + 1); + +inline std::uint16_t Unit(const std::uint8_t* bytes, std::size_t index, std::size_t width) { + if (width == 1) return bytes[index]; + return static_cast(bytes[2 * index] | bytes[2 * index + 1] << 8); +} + +inline bool IsText(std::uint16_t unit) { + return (unit >= 0x20 && unit < 0x7f) || unit == '\t' || unit == '\n' || unit == '\r'; +} + +inline bool StartsWithText(const std::uint8_t* bytes, std::size_t size, std::size_t width) { + if (size < MinimumCharacters * width) return false; + for (std::size_t index = 0; index < MinimumCharacters; ++index) { + if (!IsText(Unit(bytes, index, width))) return false; + } + return true; +} + +inline bool Format(const std::uint8_t* bytes, std::size_t size, char* out, std::size_t capacity) { + const std::size_t width = StartsWithText(bytes, size, 2) ? 2 : StartsWithText(bytes, size, 1) ? 1 : 0; + if (width == 0 || capacity < 8) return false; + std::size_t length = 0; + if (width == 2) out[length++] = 'u'; + out[length++] = '"'; + bool terminated = false; + for (std::size_t index = 0; (index + 1) * width <= size; ++index) { + const std::uint16_t unit = Unit(bytes, index, width); + if (unit == 0) { + terminated = true; + break; + } + if (index == MaximumCharacters || !IsText(unit)) break; + const char escape = unit == '\n' ? 'n' : unit == '\r' ? 'r' : unit == '\t' ? 't' : unit == '"' || unit == '\\' ? static_cast(unit) : '\0'; + if (length + (escape != '\0' ? 2 : 1) + 5 > capacity) break; + if (escape != '\0') out[length++] = '\\'; + out[length++] = escape != '\0' ? escape : static_cast(unit); + } + out[length++] = '"'; + if (!terminated) { + for (int dot = 0; dot < 3; ++dot) out[length++] = '.'; + } + out[length] = '\0'; + return true; +} + +} + +#endif diff --git a/core/libs/prx/libc/include/FailureReport.hpp b/core/libs/prx/libc/include/FailureReport.hpp new file mode 100644 index 0000000000..7a05284d6e --- /dev/null +++ b/core/libs/prx/libc/include/FailureReport.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FAILUREREPORT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_FAILUREREPORT_HPP + +extern "C" void LibcRegisterFailureReport_nid_no_patch(void (*report)() noexcept); +extern "C" void LibcUnregisterFailureReport_nid_no_patch(void (*report)() noexcept) noexcept; +extern "C" void LibcRunFailureReports_nid_no_patch() noexcept; + +#endif diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 4ad9fa00b8..0f51bc7d6d 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -71,6 +71,10 @@ public: return _handle; } + bool IsOpen() const { + return _handle != nullptr; + } + bool IsDynamic() const { return _dynamic; } @@ -103,6 +107,7 @@ public: return true; } void SyncStatus() { + if (!_handle) return; _guest.readRemaining = 0; _guest.writeRemaining = 0; _guest.flags = static_cast((_guest.flags & ~0x60) | @@ -110,6 +115,7 @@ public: } void SyncUnlockedStatus() { + if (!_handle) return; _guest.readRemaining = 0; _guest.writeRemaining = 0; #ifdef _WIN32 @@ -129,6 +135,7 @@ public: } void ClearError() { + if (!_handle) return; std::clearerr(GetHandle()); guestError = false; SyncStatus(); diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index c24d52e44e..038b5f04f7 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -25,9 +25,9 @@ inline int FilesystemError(const std::error_code& error) { 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, bool module = false) { +inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false, bool archived = false) { try { - resolved = module ? ResolvePathOrRelinkedModule_nid_no_patch(path) : ResolvePath_nid_no_patch(path); + resolved = archived ? ResolveArchivedPath_nid_no_patch(path, module) : module ? ResolvePathOrRelinkedModule_nid_no_patch(path) : ResolvePath_nid_no_patch(path); return 0; } catch (const std::filesystem::filesystem_error& error) { if (error.code() == std::errc::illegal_byte_sequence) return 2; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 37788afcfb..df34c7d52b 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -19,6 +19,8 @@ extern "C" int HostStandardError_nid_no_patch(); extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); +extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); +extern "C" std::filesystem::path ResolveDiskPath_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); @@ -27,6 +29,8 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to); // 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); extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix); +using PathProvider = std::filesystem::path (*)(const std::filesystem::path& root); +extern "C" void AddPathProvider_nid_no_patch(const char* guestPath, PathProvider provide); extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path); extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index bee10a9750..7a5dce9020 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -53,10 +53,8 @@ using Mapped = std::vector>; extern "C" { void* GuestAllocationsBegin_nid_postfix(); void GuestAllocationsRegisterMainImage_nid_postfix(void* mutation); -#ifdef _WIN32 void GuestAllocationsRegisterImage_nid_postfix(void* mutation, const void* image); void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image); -#endif void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept; void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu); void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); @@ -95,10 +93,8 @@ public: Mutation(const Mutation&) = delete; Mutation& operator=(const Mutation&) = delete; void RegisterMainImage() { GuestAllocationsRegisterMainImage_nid_postfix(handle); } -#ifdef _WIN32 void RegisterImage(const void* image) { GuestAllocationsRegisterImage_nid_postfix(handle, image); } void UnregisterImage(const void* image) { GuestAllocationsUnregisterImage_nid_postfix(handle, image); } -#endif void Add(void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu) { GuestAllocationsAdd_nid_postfix(handle, pointer, bytes, readable, writable, gpu); } void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } diff --git a/core/libs/prx/libc/include/HostErrno.hpp b/core/libs/prx/libc/include/HostErrno.hpp new file mode 100644 index 0000000000..5cb2a0853c --- /dev/null +++ b/core/libs/prx/libc/include/HostErrno.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTERRNO_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTERRNO_HPP + +extern "C" int GuestErrnoFromHost_nid_no_patch(int error); + +#endif diff --git a/core/libs/prx/libc/include/PreciseWait.hpp b/core/libs/prx/libc/include/PreciseWait.hpp index 73249384bb..e1829564db 100644 --- a/core/libs/prx/libc/include/PreciseWait.hpp +++ b/core/libs/prx/libc/include/PreciseWait.hpp @@ -1,21 +1,44 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_PRECISEWAIT_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_PRECISEWAIT_HPP +#include #include +#include #include +#include + +inline constexpr std::uint64_t PreciseHundredNanosToQpcTicks(unsigned long long hundredNanos, std::uint64_t freq) { + if (freq == 10000000ULL) [[likely]] { + return hundredNanos; + } + const std::uint64_t wholeSeconds = hundredNanos / 10000000ULL; + const std::uint64_t remHundredNanos = hundredNanos % 10000000ULL; + return wholeSeconds * freq + (remHundredNanos * freq + 9999999ULL) / 10000000ULL; +} + +inline constexpr std::uint64_t PrecisePauseThresholdTicks(std::uint64_t freq) { + return PreciseHundredNanosToQpcTicks(2ULL, freq); +} #ifdef _WIN32 #include #include "prx/libc/include/HostThreadLocal.hpp" -#endif -inline constexpr unsigned long long kSubTickWaitUs = 2000; - -inline void PreciseSleepUs(unsigned long long micros) { - if (micros == 0) { - return; +inline void PreciseSleepWaitableTimerWithHandle(HANDLE handle, unsigned long long sleepHundredNanos) { + bool slept = false; + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) [[likely]] { + LARGE_INTEGER due; + due.QuadPart = -static_cast(sleepHundredNanos); + if (SetWaitableTimer(handle, &due, 0, nullptr, nullptr, FALSE) != FALSE) [[likely]] { + slept = (WaitForSingleObject(handle, INFINITE) == WAIT_OBJECT_0); + } } -#ifdef _WIN32 + if (!slept) [[unlikely]] { + std::this_thread::sleep_for(std::chrono::nanoseconds(sleepHundredNanos * 100ULL)); + } +} + +inline HANDLE PreciseWaitThreadTimer() { struct Timer { HANDLE handle = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); @@ -25,24 +48,205 @@ inline void PreciseSleepUs(unsigned long long micros) { } } }; - auto& timer = HostThreadLocal(); - if (timer.handle != nullptr) { - LARGE_INTEGER due{}; - due.QuadPart = -static_cast(micros * 10ULL); - if (SetWaitableTimerEx(timer.handle, &due, 0, nullptr, nullptr, nullptr, 0) != FALSE) { - WaitForSingleObject(timer.handle, INFINITE); - return; + return HostThreadLocal().handle; +} + +inline void PreciseSleepFromCounterWithTimer( + LARGE_INTEGER counter, + unsigned long long hundredNanos, + HANDLE timerHandle, + bool useCustomTimer, + bool allowSpinTail = true) { + if (hundredNanos == 0) { + return; + } + constexpr unsigned long long kSpinTailHundredNanos = 2500; + constexpr unsigned long long kMinTimerSpinSleepHundredNanos = 10000; + static std::atomic cachedFreq{0}; + std::uint64_t freq = cachedFreq.load(std::memory_order_relaxed); + std::uint64_t deltaTicks = hundredNanos; + std::uint64_t pauseThresholdTicks = 2ULL; + if (freq != 10000000ULL) [[unlikely]] { + if (freq == 0) { + LARGE_INTEGER f; + QueryPerformanceFrequency(&f); + freq = static_cast(f.QuadPart); + cachedFreq.store(freq, std::memory_order_relaxed); + } + if (freq != 10000000ULL) { + deltaTicks = PreciseHundredNanosToQpcTicks(hundredNanos, freq); + pauseThresholdTicks = PrecisePauseThresholdTicks(freq); + } + } + const std::uint64_t deadlineTicks = static_cast(counter.QuadPart) + deltaTicks; + const bool useSpinTail = allowSpinTail || hundredNanos >= kMinTimerSpinSleepHundredNanos; + if (!useSpinTail || hundredNanos > kSpinTailHundredNanos) [[unlikely]] { + const HANDLE handle = useCustomTimer ? timerHandle : PreciseWaitThreadTimer(); + const unsigned long long timerHundredNanos = useSpinTail ? (hundredNanos - kSpinTailHundredNanos) : hundredNanos; + PreciseSleepWaitableTimerWithHandle(handle, timerHundredNanos); + } + for (;;) { + QueryPerformanceCounter(&counter); + const std::uint64_t nowTicks = static_cast(counter.QuadPart); + if (nowTicks >= deadlineTicks) { + break; + } + if (deadlineTicks - nowTicks > pauseThresholdTicks) { + YieldProcessor(); + } + } +} + +inline void PreciseSleepFromCounter(LARGE_INTEGER counter, unsigned long long hundredNanos) { + PreciseSleepFromCounterWithTimer(counter, hundredNanos, nullptr, false, true); +} + +inline void PreciseSleepHundredNanos(unsigned long long hundredNanos) { + if (hundredNanos == 0) { + return; + } + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + PreciseSleepFromCounterWithTimer(counter, hundredNanos, nullptr, false, false); +} +#elif !defined(__EMSCRIPTEN__) +#include +#include +#ifdef __linux__ +#include +#endif +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) +#include +#endif + +inline void PreciseSpinPause() { +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) + _mm_pause(); +#else + std::atomic_signal_fence(std::memory_order_seq_cst); +#endif +} + +inline std::uint64_t PreciseMonotonicNanos() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1000000000ULL + static_cast(ts.tv_nsec); +} + +#ifdef __linux__ +inline bool PreciseLinuxLowTimerSlack() { + static thread_local const bool lowSlack = [] { + const long current = prctl(PR_GET_TIMERSLACK, 0UL, 0UL, 0UL, 0UL); + return current == 1L || prctl(PR_SET_TIMERSLACK, 1UL, 0UL, 0UL, 0UL) == 0; + }(); + return lowSlack; +} +#endif + +inline void PreciseSleepFromNanosWithSlack(std::uint64_t startNanos, unsigned long long nanos, bool allowLowSlack) { + if (nanos == 0) { + return; + } + const std::uint64_t deadlineNanos = startNanos + nanos; +#ifdef __linux__ + const bool lowSlack = allowLowSlack && PreciseLinuxLowTimerSlack(); + const unsigned long long spinTailNanos = lowSlack ? 15000ULL : 100000ULL; + if (nanos > spinTailNanos) [[unlikely]] { + const std::uint64_t wakeNanos = deadlineNanos - spinTailNanos; + struct timespec req; + req.tv_sec = static_cast(wakeNanos / 1000000000ULL); + req.tv_nsec = static_cast(wakeNanos % 1000000000ULL); + while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &req, nullptr) == EINTR) { + } + } +#else + (void)allowLowSlack; + constexpr unsigned long long kSpinTailNanos = 100000ULL; + if (nanos > kSpinTailNanos) [[unlikely]] { + const unsigned long long sleepNanos = nanos - kSpinTailNanos; + struct timespec req; + req.tv_sec = static_cast(sleepNanos / 1000000000ULL); + req.tv_nsec = static_cast(sleepNanos % 1000000000ULL); + while (nanosleep(&req, &req) == -1 && errno == EINTR) { } } #endif + for (;;) { + const std::uint64_t nowNanos = PreciseMonotonicNanos(); + if (nowNanos >= deadlineNanos) { + break; + } + if (deadlineNanos - nowNanos > 200ULL) { + PreciseSpinPause(); + } + } +} + +inline void PreciseSleepFromNanos(std::uint64_t startNanos, unsigned long long nanos) { + PreciseSleepFromNanosWithSlack(startNanos, nanos, true); +} + +inline void PreciseSleepNanos(unsigned long long nanos) { + if (nanos == 0) { + return; + } + PreciseSleepFromNanos(PreciseMonotonicNanos(), nanos); +} +#endif + +inline constexpr unsigned long long kSubTickWaitUs = 2000; + +inline void PreciseSleepUs(unsigned long long micros) { + if (micros == 0) { + return; + } +#ifdef _WIN32 + PreciseSleepHundredNanos(micros * 10ULL); + return; +#elif !defined(__EMSCRIPTEN__) + PreciseSleepNanos(micros * 1000ULL); + return; +#else std::this_thread::sleep_for(std::chrono::microseconds(micros)); +#endif } template inline void PreciseSleepUntil(const std::chrono::time_point& deadline) { +#ifdef _WIN32 + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + const auto now = TClock::now(); + if (now < deadline) { + const auto remainingNs = std::chrono::duration_cast(deadline - now).count(); + if (remainingNs > 0) { + PreciseSleepFromCounter(counter, (static_cast(remainingNs) + 99ULL) / 100ULL); + } + } + for (auto current = TClock::now(); current < deadline; current = TClock::now()) { + if (deadline - current > std::chrono::nanoseconds(200)) { + YieldProcessor(); + } + } +#elif !defined(__EMSCRIPTEN__) + const std::uint64_t startNanos = PreciseMonotonicNanos(); + const auto now = TClock::now(); + if (now < deadline) { + const auto remainingNs = std::chrono::duration_cast(deadline - now).count(); + if (remainingNs > 0) { + PreciseSleepFromNanos(startNanos, static_cast(remainingNs)); + } + } + for (auto current = TClock::now(); current < deadline; current = TClock::now()) { + if (deadline - current > std::chrono::nanoseconds(200)) { + PreciseSpinPause(); + } + } +#else for (auto now = TClock::now(); now < deadline; now = TClock::now()) { PreciseSleepUs(static_cast(std::chrono::ceil(deadline - now).count())); } +#endif } #endif diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index 4bfe737f75..8e0f9ec209 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -11,8 +11,11 @@ extern "C" std::stop_token LibcShutdownToken_nid_postfix(); extern "C" void LibcRequestShutdown_nid_postfix(); extern "C" void LibcRequestExit_nid_postfix(int code); extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix(); +extern "C" void LibcStartExit_nid_postfix(); extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); +extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()); +extern "C" void LibcRunEndFlush_nid_no_patch(); #endif diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 39a655f51e..497f1c40aa 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -226,6 +226,20 @@ inline __int128 SonyDivti3(__int128 dividend, __int128 divisor) { return (__int128)(negative ? (unsigned __int128)0 - magnitude : magnitude); } +inline __int128 SonyModti3(__int128 dividend, __int128 divisor) { + if (divisor == 0) throw std::runtime_error("__modti3: integer divide by zero"); + const bool negative = dividend < 0; + const unsigned __int128 magnitudeDividend = negative + ? (unsigned __int128)0 - (unsigned __int128)dividend + : (unsigned __int128)dividend; + const unsigned __int128 magnitudeDivisor = divisor < 0 + ? (unsigned __int128)0 - (unsigned __int128)divisor + : (unsigned __int128)divisor; + unsigned __int128 remainder = 0; + SonyUdivmod128(magnitudeDividend, magnitudeDivisor, &remainder); + return (__int128)(negative ? (unsigned __int128)0 - remainder : remainder); +} + } #endif diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index f3deb658df..436549ba2c 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -327,6 +327,10 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Value(spec + "ll" + conversion, value); } else if (std::strchr("ouxX", conversion)) { if (!integerLength) throw std::invalid_argument("Invalid integer length"); + for (size_t flag = 1; flag < spec.size() && std::strchr("-+ #0", spec[flag]);) { + if (spec[flag] == ' ' || spec[flag] == '+') spec.erase(flag, 1); + else ++flag; + } unsigned long long value; if (length.empty() || length == "h" || length == "hh") { value = args.Next(); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 56c8851fbe..9a167663fa 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -261,6 +261,9 @@ public: if (found == views.end() || !found->second.resident) continue; const auto run = std::prev(residentRuns.upper_bound(address + done)); if (run->first < address || run->second.end > address + bytes) throw refuse("a resident run of direct memory extends outside the range"); + for (auto page = views.find(run->first); page != views.end() && page->first < run->second.end; ++page) { + if (page->second.hostWrites != 0) return nullptr; + } } for (std::size_t done = 0; done < bytes; done += pageBytes) { const auto found = views.find(address + done); diff --git a/core/libs/prx/libc/include/general/LogMacros.hpp b/core/libs/prx/libc/include/general/LogMacros.hpp index 46ba77734e..00e511b24b 100644 --- a/core/libs/prx/libc/include/general/LogMacros.hpp +++ b/core/libs/prx/libc/include/general/LogMacros.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP +#include #include #include #include @@ -8,6 +9,25 @@ #include #include +using LogSinkFn = int (*)(std::FILE* stream, const char* text, std::size_t length); + +inline std::atomic& LogSink() { + static std::atomic sink{nullptr}; + return sink; +} + +inline long long LogTimestampMicros() { + static const bool timestamps = std::getenv("APS5_LOG_TIMESTAMPS") != nullptr; + if (!timestamps) return -1; + using Clock = std::chrono::steady_clock; + static const auto started = Clock::now(); + return static_cast(std::chrono::duration_cast(Clock::now() - started).count()); +} + +inline int FormatLogTimestamp(char* out, std::size_t capacity, long long micros) { + return std::snprintf(out, capacity, "[+%llu.%06llu s] ", static_cast(micros / 1000000), static_cast(micros % 1000000)); +} + #ifdef __MINGW_PRINTF_FORMAT [[gnu::format(__MINGW_PRINTF_FORMAT, 2, 3)]] #else @@ -16,14 +36,9 @@ inline int WriteLogLine(std::FILE* stream, const char* format, ...) { char small[1024]; std::size_t prefixLength = 0; - static const bool timestamps = std::getenv("APS5_LOG_TIMESTAMPS") != nullptr; - if (timestamps) { - using Clock = std::chrono::steady_clock; - static const auto started = Clock::now(); - const auto elapsed = std::chrono::duration_cast(Clock::now() - started).count(); - const auto seconds = static_cast(elapsed / 1000000); - const auto microseconds = static_cast(elapsed % 1000000); - const int written = std::snprintf(small, sizeof(small), "[+%llu.%06llu s] ", seconds, microseconds); + const long long micros = LogTimestampMicros(); + if (micros >= 0) { + const int written = FormatLogTimestamp(small, sizeof(small), micros); if (written < 0 || written >= static_cast(sizeof(small))) return -1; prefixLength = static_cast(written); } @@ -43,7 +58,9 @@ inline int WriteLogLine(std::FILE* stream, const char* format, ...) { va_end(again); if (length < 0) return length; const auto totalLength = prefixLength + static_cast(length); - std::fwrite(large.empty() ? small : large.data(), 1, totalLength, stream); + const char* text = large.empty() ? small : large.data(); + if (const auto sink = LogSink().load(std::memory_order_acquire)) return sink(stream, text, totalLength); + std::fwrite(text, 1, totalLength, stream); return std::fflush(stream); } diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp index 2e91cc2159..1605fbd125 100644 --- a/core/libs/prx/libc/src/AllocatingStrings.cpp +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args); char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit) { + if (!source) { errno = 22; return nullptr; } std::size_t length = 0; while (length < limit && source[length] != '\0') ++length; try { diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp new file mode 100644 index 0000000000..be3916a765 --- /dev/null +++ b/core/libs/prx/libc/src/AppArchive.cpp @@ -0,0 +1,239 @@ +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/General.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Archive { + std::mutex mutex; + bool opened = false; + std::unique_ptr reader; + std::filesystem::path path; + std::filesystem::path app0; +}; + +Archive& State() { + static Archive archive; + return archive; +} + +bool SameComponent(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + const auto fold = [](unsigned char value) { return value >= 'A' && value <= 'Z' ? value + ('a' - 'A') : value; }; + for (std::size_t i = 0; i < left.size(); ++i) if (fold(left[i]) != fold(right[i])) return false; + return true; +} + +Archive& Opened() { + auto& archive = State(); + std::lock_guard lock(archive.mutex); + if (archive.opened) return archive; + const auto app0 = ResolveDiskPath_nid_no_patch("/app0").lexically_normal(); + auto candidate = app0; + candidate += ".zar"; + std::error_code error; + if (std::filesystem::is_regular_file(candidate, error)) { + archive.reader.reset(ZArchiveReader::OpenFromFile(candidate)); + if (!archive.reader) throw std::runtime_error("app0.zar is not a readable ZArchive: " + candidate.string()); + archive.path = candidate; + } + archive.app0 = app0; + archive.opened = true; + return archive; +} + +bool ArchiveRelative(const Archive& archive, const std::filesystem::path& hostPath, std::string& relative) { + const auto normal = hostPath.lexically_normal(); + auto part = normal.begin(); + for (const auto& component : archive.app0) { + if (part == normal.end() || !SameComponent(part->generic_string(), component.generic_string())) return false; + ++part; + } + relative.clear(); + for (; part != normal.end(); ++part) { + const auto name = part->generic_string(); + if (name.empty() || name == ".") continue; + if (name == "..") return false; + if (!relative.empty()) relative += '/'; + relative += name; + } + return true; +} + +struct OpenFile { + AppArchiveNode node; + std::uint64_t position = 0; + std::mutex mutex; +}; + +std::mutex g_descriptorMutex; +std::map> g_descriptors; + +std::shared_ptr Attached(int descriptor) { + std::lock_guard lock(g_descriptorMutex); + const auto found = g_descriptors.find(descriptor); + return found == g_descriptors.end() ? nullptr : found->second; +} + +ZArchiveReader& Reader(std::uint32_t handle) { + auto& archive = Opened(); + if (!archive.reader) throw std::logic_error("app0.zar handle " + std::to_string(handle) + " used without an archive"); + return *archive.reader; +} + +} + +extern "C" { + +bool AppArchiveLookup_nid_no_patch(const std::filesystem::path& hostPath, AppArchiveNode* node) { + if (node == nullptr) throw std::invalid_argument("AppArchiveLookup: null node"); + auto& archive = Opened(); + if (!archive.reader) return false; + std::string relative; + if (!ArchiveRelative(archive, hostPath, relative)) return false; + const auto handle = archive.reader->LookUp(relative); + if (handle == ZARCHIVE_INVALID_NODE) return false; + node->Handle = handle; + node->Directory = archive.reader->IsDirectory(handle); + node->Size = node->Directory ? 0 : archive.reader->GetFileSize(handle); + return true; +} + +std::uint64_t AppArchiveRead_nid_no_patch(std::uint32_t handle, std::uint64_t offset, void* buffer, std::uint64_t length) { + auto& reader = Reader(handle); + if (!reader.IsFile(handle)) throw std::invalid_argument("AppArchiveRead: handle " + std::to_string(handle) + " is not a file"); + const auto size = reader.GetFileSize(handle); + if (length == 0 || offset >= size) return 0; + if (buffer == nullptr) throw std::invalid_argument("AppArchiveRead: null buffer"); + const auto expected = std::min(length, size - offset); + const auto read = reader.ReadFromFile(handle, offset, length, buffer); + if (read != expected) throw std::runtime_error("app0.zar is damaged: a compressed block of handle " + std::to_string(handle) + " cannot be read"); + return read; +} + +std::vector AppArchiveList_nid_no_patch(std::uint32_t handle) { + auto& reader = Reader(handle); + if (!reader.IsDirectory(handle)) throw std::invalid_argument("AppArchiveList: handle " + std::to_string(handle) + " is not a directory"); + std::vector entries; + const auto count = reader.GetDirEntryCount(handle); + entries.reserve(count); + for (std::uint32_t index = 0; index < count; ++index) { + ZArchiveReader::DirEntry entry{}; + if (!reader.GetDirEntry(handle, index, entry)) throw std::runtime_error("app0.zar is damaged: directory entry " + std::to_string(index) + " cannot be read"); + entries.push_back({std::string(entry.name), entry.isDirectory}); + } + return entries; +} + +void AppArchiveReady_nid_no_patch() { + (void)Opened(); +} + +void AppArchiveRefuseOnly_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath, const char* operation) { + if (AppArchiveOnly_nid_no_patch(hostPath)) NotImplemented_nid_no_patch((std::string(operation) + " " + guestPath + " in app0.zar").c_str()); +} + +void AppArchiveRefuseMergedListing_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath) { + if (AppArchiveHasDirectory_nid_no_patch(hostPath)) NotImplemented_nid_no_patch((std::string("listing ") + guestPath + " merged with app0.zar").c_str()); +} + +void AppArchiveRefuseDescriptor_nid_no_patch(int descriptor, const char* operation) { + if (AppArchiveDescriptor_nid_no_patch(descriptor, nullptr, nullptr)) NotImplemented_nid_no_patch((std::string(operation) + " of a file read from app0.zar").c_str()); +} + +std::filesystem::path AppArchivePath_nid_no_patch() { + return Opened().path; +} + +bool AppArchiveOnly_nid_no_patch(const std::filesystem::path& hostPath) { + auto& archive = Opened(); + if (!archive.reader) return false; + std::error_code error; + const auto status = std::filesystem::symlink_status(hostPath, error); + if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) return false; + AppArchiveNode node{}; + return AppArchiveLookup_nid_no_patch(hostPath, &node); +} + +bool AppArchiveHasDirectory_nid_no_patch(const std::filesystem::path& hostPath) { + AppArchiveNode node{}; + return AppArchiveLookup_nid_no_patch(hostPath, &node) && node.Directory; +} + +void AppArchiveAttach_nid_no_patch(int descriptor, const AppArchiveNode& node) { + if (node.Directory) throw std::invalid_argument("AppArchiveAttach: descriptor " + std::to_string(descriptor) + " cannot hold an app0.zar directory"); + auto file = std::make_shared(); + file->node = node; + std::lock_guard lock(g_descriptorMutex); + g_descriptors[descriptor] = std::move(file); +} + +void AppArchiveShare_nid_no_patch(int from, int to) { + std::lock_guard lock(g_descriptorMutex); + const auto found = g_descriptors.find(from); + if (found == g_descriptors.end()) g_descriptors.erase(to); + else g_descriptors[to] = found->second; +} + +void AppArchiveDetach_nid_no_patch(int descriptor) { + std::lock_guard lock(g_descriptorMutex); + g_descriptors.erase(descriptor); +} + +bool AppArchiveDescriptor_nid_no_patch(int descriptor, AppArchiveNode* node, std::uint64_t* position) { + const auto file = Attached(descriptor); + if (!file) return false; + std::lock_guard lock(file->mutex); + if (node != nullptr) *node = file->node; + if (position != nullptr) *position = file->position; + return true; +} + +bool AppArchiveDescriptorRead_nid_no_patch(int descriptor, const AppArchiveBuffer* buffers, int count, const std::int64_t* offset, std::uint64_t* read) { + const auto file = Attached(descriptor); + if (!file) return false; + if (read == nullptr || count < 0 || (buffers == nullptr && count != 0)) throw std::invalid_argument("AppArchiveDescriptorRead: invalid arguments for descriptor " + std::to_string(descriptor)); + if (offset != nullptr && *offset < 0) throw std::invalid_argument("AppArchiveDescriptorRead: negative offset for descriptor " + std::to_string(descriptor)); + std::lock_guard lock(file->mutex); + auto cursor = offset != nullptr ? static_cast(*offset) : file->position; + std::uint64_t total = 0; + for (int i = 0; i < count; ++i) { + const auto done = AppArchiveRead_nid_no_patch(file->node.Handle, cursor, buffers[i].Base, buffers[i].Length); + cursor += done; + total += done; + if (done < buffers[i].Length) break; + } + if (offset == nullptr) file->position = cursor; + *read = total; + return true; +} + +bool AppArchiveDescriptorSeek_nid_no_patch(int descriptor, std::int64_t offset, int whence, std::int64_t* position) { + const auto file = Attached(descriptor); + if (!file) return false; + if (position == nullptr) throw std::invalid_argument("AppArchiveDescriptorSeek: null position"); + std::lock_guard lock(file->mutex); + constexpr auto limit = std::numeric_limits::max(); + std::int64_t base = 0; + if (whence == 1) base = static_cast(file->position); + else if (whence == 2) base = static_cast(file->node.Size); + else if (whence != 0) throw std::invalid_argument("AppArchiveDescriptorSeek: invalid whence " + std::to_string(whence)); + if ((offset > 0 && base > limit - offset) || base + offset < 0) { + *position = -1; + return true; + } + file->position = static_cast(base + offset); + *position = base + offset; + return true; +} + +} diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 6acd014d08..34b89291d4 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -202,6 +202,20 @@ void* ApplicationHeapAlign_nid_no_patch(std::size_t alignment, std::size_t bytes return pointer; } +extern "C" void* APS5_VABI scriptingGetMem_nid_postfix(std::size_t alignment, std::size_t bytes) { + alignment = std::max(alignment, 16); + if ((alignment & (alignment - 1)) != 0) return nullptr; + try { + return ApplicationHeapAlign_nid_no_patch(alignment, bytes); + } catch (const std::bad_alloc&) { + return nullptr; + } +} + +extern "C" void APS5_VABI scriptingFreeMem_nid_postfix(void* pointer) { + ApplicationHeapFree_nid_no_patch(pointer); +} + void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std::size_t alignment) { if (bytes == 0) { ApplicationHeapFree_nid_no_patch(pointer); diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 5286225707..29c7dae111 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -1,16 +1,116 @@ #include +#include +#include +#include +#include +#include + #include "prx/libc/include/General.hpp" +#ifdef __linux__ +#include +#include + +namespace { + +constexpr int MaxFrames = 256; +constexpr std::size_t NameOffset = 0x10; +constexpr char GuestSuffix[] = ".guest.prx"; + +struct Frame { + std::uint32_t offset; + std::string module; +}; + +struct FrameRecord { + FrameRecord* next; + std::uint32_t offset; + std::uint32_t reserved; +}; +static_assert(sizeof(FrameRecord) == NameOffset); + +[[gnu::noinline]] std::vector CaptureFrames(int limit) { + void* addresses[MaxFrames]; + const int captured = backtrace(addresses, MaxFrames); + std::vector frames; + for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { + Dl_info info{}; + if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); + if (offset > UINT32_MAX) continue; + std::string module = info.dli_fname; + if (const auto slash = module.find_last_of('/'); slash != std::string::npos) module.erase(0, slash + 1); + if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) + module.resize(module.size() - (sizeof(GuestSuffix) - 1)); + frames.push_back({static_cast(offset), std::move(module)}); + } + return frames; +} + +std::size_t RecordSize(const Frame& frame) { + return (NameOffset + frame.module.size() + 1 + 7) & ~std::size_t{7}; +} + +} // namespace + extern "C" { -int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*) { +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int limit, int* count, std::size_t* size) { + if (count == nullptr || size == nullptr) throw std::invalid_argument("sceLibcBacktraceGetBufferSize: null output"); + const auto frames = CaptureFrames(limit); + std::size_t total = 0; + for (const auto& frame : frames) total += RecordSize(frame); + *count = static_cast(frames.size()); + *size = total; + return 0; +} + +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int limit, void* buffer, std::size_t size, int* count) { + if (buffer == nullptr || count == nullptr) throw std::invalid_argument("sceLibcBacktraceSelf: null argument"); + const auto frames = CaptureFrames(limit); + auto* cursor = static_cast(buffer); + FrameRecord* previous = nullptr; + int written = 0; + for (const auto& frame : frames) { + const auto bytes = RecordSize(frame); + if (static_cast(cursor - static_cast(buffer)) + bytes > size) break; + auto* record = reinterpret_cast(cursor); + record->next = nullptr; + record->offset = frame.offset; + record->reserved = 0; + std::memcpy(cursor + NameOffset, frame.module.c_str(), frame.module.size() + 1); + if (previous != nullptr) previous->next = record; + previous = record; + cursor += bytes; + ++written; + } + *count = written; + return 0; +} + +} + +#else + +extern "C" { + +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int limit, int* count, std::size_t* size) { + (void)limit; + (void)count; + (void)size; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*) { +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int limit, void* buffer, std::size_t size, int* count) { + (void)limit; + (void)buffer; + (void)size; + (void)count; NotImplemented_nid_no_patch(__func__); return 0; } } + +#endif diff --git a/core/libs/prx/libc/src/BinaryWordIo.cpp b/core/libs/prx/libc/src/BinaryWordIo.cpp new file mode 100644 index 0000000000..733aadff22 --- /dev/null +++ b/core/libs/prx/libc/src/BinaryWordIo.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +static_assert(sizeof(int) == 4 && std::endian::native == std::endian::little); + +extern "C" { +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI putc_unlocked_nid_postfix(int, FileStream*); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); +} + +namespace { + +class LockedStream { + FileStream* stream; + +public: + explicit LockedStream(FileStream* value) : stream(value) { flockfile_nid_postfix(stream); } + ~LockedStream() { funlockfile_nid_postfix(stream); } +}; + +} + +extern "C" int APS5_VABI getw_nid_postfix(FileStream* stream) { + int value = 0; + if (fread_nid_postfix(&value, sizeof(value), 1, stream) != 1) return -1; + return value; +} + +extern "C" int APS5_VABI putw_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + const LockedStream lock(stream); + const auto bits = static_cast(value); + for (unsigned shift = 0; shift < 32; shift += 8) + if (putc_unlocked_nid_postfix((bits >> shift) & 0xff, stream) == -1) return -1; + return 0; +} diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index ff24e15a7e..d831ad7976 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/AppArchive.hpp" #include #include #include @@ -109,6 +110,7 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { try { auto directory = std::make_unique(); if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } + AppArchiveRefuseMergedListing_nid_no_patch(directory->path, path); directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } return directory.release(); diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 03996644bf..cbb171729b 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -9,10 +9,6 @@ #include #include "prx/libc/include/general/VabiMacros.hpp" -#ifndef _WIN32 -extern char** environ; -#endif - namespace { struct EnvironmentEntry { std::unique_ptr owned; @@ -26,20 +22,7 @@ struct EnvironmentEntry { }; std::mutex environmentMutex; std::vector& Environment() { - // Seed once from the host. Guest mutations remain local to the guest runtime. - static std::vector entries = [] { - std::vector result; -#ifdef _WIN32 - char** source = _environ; -#else - char** source = environ; -#endif - if (source) for (; *source; ++source) { - if (**source && **source != '=' && std::strchr(*source, '=')) - result.push_back({std::make_unique(*source), nullptr}); - } - return result; - }(); + static std::vector entries; return entries; } bool ValidEnvironmentName(const char* name) { diff --git a/core/libs/prx/libc/src/FailureReport.cpp b/core/libs/prx/libc/src/FailureReport.cpp new file mode 100644 index 0000000000..9fcb5473de --- /dev/null +++ b/core/libs/prx/libc/src/FailureReport.cpp @@ -0,0 +1,39 @@ +#include "prx/libc/include/FailureReport.hpp" +#include +#include +#include + +namespace { + +using Report = void (*)() noexcept; + +std::array, 8> reports{}; +std::atomic ran{false}; + +} + +extern "C" void LibcRegisterFailureReport_nid_no_patch(Report report) { + if (report == nullptr) throw std::invalid_argument("libc: null failure report"); + for (auto& slot : reports) { + if (slot.load(std::memory_order_acquire) == report) return; + } + for (auto& slot : reports) { + Report empty = nullptr; + if (slot.compare_exchange_strong(empty, report, std::memory_order_acq_rel)) return; + } + throw std::runtime_error("libc: more than 8 failure reports registered"); +} + +extern "C" void LibcUnregisterFailureReport_nid_no_patch(Report report) noexcept { + for (auto& slot : reports) { + Report expected = report; + slot.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel); + } +} + +extern "C" void LibcRunFailureReports_nid_no_patch() noexcept { + if (ran.exchange(true, std::memory_order_acq_rel)) return; + for (auto& slot : reports) { + if (const auto report = slot.load(std::memory_order_acquire)) report(); + } +} diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 69e7f8d0fe..4f7330bc0c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -26,6 +26,8 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/HostErrno.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/GuestArena.hpp" #ifdef __linux__ @@ -66,7 +68,16 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin const DWORD disposition = access == 'r' ? OPEN_EXISTING : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; - const auto handle = CreateFileW(path.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + auto filename = path.native(); + if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { + std::error_code pathError; + auto absolutePath = std::filesystem::absolute(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } + const auto absolute = absolutePath.lexically_normal().make_preferred().native(); + if (absolute.size() >= MAX_PATH) + filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + } + const auto handle = CreateFileW(filename.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { errno = WindowsOpenError_nid_no_patch(GetLastError()); @@ -237,6 +248,7 @@ extern "C" { FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { if (descriptor < 0) { errno = 9; return nullptr; } if (!mode) { errno = 22; return nullptr; } + AppArchiveRefuseDescriptor_nid_no_patch(descriptor, "fdopen"); const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } try { @@ -282,7 +294,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; } - const int error = errno; + const int error = GuestErrnoFromHost_nid_no_patch(errno); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; @@ -301,8 +313,7 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { - const int error = errno; - errno = error; + errno = GuestErrnoFromHost_nid_no_patch(errno); return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); @@ -315,15 +326,19 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode) { constexpr int GuestEinval = 22; - constexpr int GuestEnoent = 2; if (!result) return GuestEinval; *result = nullptr; if (!filename || !mode) return GuestEinval; *result = fopen_nid_postfix(filename, mode); - return *result ? 0 : GuestEnoent; + return *result ? 0 : errno; } int APS5_VABI fclose_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { + std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); + errno = 9; + return -1; + } GetNativeStream(stream); std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); return stream->Close(); @@ -339,17 +354,15 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const {In, "r"}, {Out, "w"}, {Out | Trunc, "w"}, {Out | App, "a"}, {In | Binary, "rb"}, {Out | Binary, "wb"}, {Out | Trunc | Binary, "wb"}, {Out | App | Binary, "ab"}, {In | Out, "r+"}, {In | Out | Trunc, "w+"}, {In | Out | App, "a+"}, - {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}}; - const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); + {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}, + {Out | Noreplace, "wx"}, {Out | Trunc | Noreplace, "wx"}, {In | Out | Trunc | Noreplace, "w+x"}, + {Out | Binary | Noreplace, "wbx"}, {Out | Trunc | Binary | Noreplace, "wbx"}, + {In | Out | Trunc | Binary | Noreplace, "w+bx"}}; + const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0) | + (((mode & Noreplace) && (mode & (Out | App))) ? Noreplace : 0); const char* openMode = nullptr; for (const auto& [flags, text] : modes) if (flags == open) openMode = text; if (!openMode) return nullptr; - if ((mode & Noreplace) && (open & Out)) { - if (auto* existing = fopen_nid_postfix(filename, "r")) { - fclose_nid_postfix(existing); - return nullptr; - } - } auto* stream = fopen_nid_postfix(filename, openMode); if (!stream || !(mode & Ate) || fseek_nid_postfix(stream, 0, SEEK_END) == 0) return stream; fclose_nid_postfix(stream); @@ -357,6 +370,7 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const } size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); @@ -371,6 +385,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File } size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t count, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fwrite: null buffer"); @@ -380,6 +395,7 @@ 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 (stream && !stream->IsOpen()) { errno = 9; return -1; } if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) { errno = 22; return -1; } #ifdef _WIN32 auto* native = GetNativeStream(stream); @@ -396,6 +412,7 @@ int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int or } std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 auto* native = GetNativeStream(stream); if (IsPipeStream(native)) { errno = 29; return -1; } @@ -435,12 +452,14 @@ void APS5_VABI rewind_nid_postfix(FileStream* stream) { int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { if (!str) throw std::runtime_error("fputs: null string"); + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fputs(str, GetNativeStream(stream)); stream->SyncStatus(); return result; } int APS5_VABI fflush_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } std::FILE* native = stream ? GetNativeStream(stream) : nullptr; #ifdef __linux__ if (native && !__fwriting(native)) return 0; diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index bc08460a33..7a77dad2de 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -142,6 +142,7 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n if (conversion == 'n') { if (!suppress) { void* target = nextPointer(); + if (target == nullptr) return EOF; const auto count = input - buffer; if (length == "hh") *static_cast(target) = static_cast(count); else if (length == "h") *static_cast(target) = static_cast(count); @@ -163,7 +164,10 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n std::string directive = "%"; void* target = nullptr; if (suppress) directive += '*'; - else target = nextPointer(); + else { + target = nextPointer(); + if (target == nullptr) return EOF; + } if (sized && !suppress) { const auto capacity = static_cast(nextCapacity()); const unsigned long limit = conversion == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); @@ -229,6 +233,10 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const char* source = guestWide ? guestText.c_str() : text; #endif auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); + if (!output) { + errno = 12; + return -1; + } std::memcpy(output, source, static_cast(count) + 1); *destination = output; return count; @@ -239,6 +247,7 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi } int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); #ifdef _WIN32 std::string buffer; @@ -280,6 +289,7 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { } int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); #ifdef _WIN32 __builtin_sysv_va_list args; @@ -300,6 +310,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { #ifdef _WIN32 int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -308,6 +319,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { } int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -334,6 +346,7 @@ int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { #else int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); @@ -342,6 +355,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { } int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); @@ -423,6 +437,7 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #endif int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 return LibcDetail::PrintWindows(str, c); #else @@ -541,6 +556,7 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #endif int APS5_VABI puts_nid_postfix(const char* s) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } return std::puts(s); } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index e9a5661781..713a520591 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -215,6 +215,9 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = } // namespace extern "C" { +extern FileStream* __stdoutp_nid_postfix; +std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream); +int APS5_VABI ferror_nid_postfix(FileStream* stream); int APS5_VABI vswprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { if (buffer == nullptr || size == 0) { errno = 22; return -1; } @@ -359,4 +362,33 @@ int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { } } +int APS5_VABI putwc_nid_postfix(char16_t value, FileStream* stream) { + return fputwc_nid_postfix(value, stream); +} + +int APS5_VABI putwchar_nid_postfix(char16_t value) { + return fputwc_nid_postfix(value, __stdoutp_nid_postfix); +} + +char16_t* APS5_VABI fgetws_nid_postfix(char16_t* buffer, int size, FileStream* stream) { + if (buffer == nullptr || stream == nullptr || size <= 0) { + if (stream != nullptr) stream->SetError(); + errno = 22; + return nullptr; + } + int count = 0; + while (count < size - 1) { + const std::int32_t character = fgetwc_nid_postfix(stream); + if (character == -1) { + if (count == 0) return nullptr; + break; + } + buffer[count++] = static_cast(character); + if (character == static_cast(u'\n')) break; + } + if (ferror_nid_postfix(stream) != 0) return nullptr; + buffer[count] = 0; + return buffer; +} + } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 69c32c1513..0f18419dfd 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -31,6 +31,7 @@ #include #endif #include "prx/libc/include/General.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/ApplicationHeap.hpp" namespace { @@ -40,6 +41,7 @@ struct PathAliases { std::mutex mutex; std::vector> entries; std::vector blocked; + std::vector> providers; }; PathAliases& Aliases() { @@ -169,8 +171,27 @@ WorkingDirectory& Directories() { static auto* state = new WorkingDirectory; return *state; } +std::optional ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) { + const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str()); + PathProvider provide = nullptr; + { + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + for (const auto& [path, provider] : aliases.providers) { + if (SameName(relative, path)) provide = provider; + } + } + if (provide == nullptr) return std::nullopt; + const auto supplied = ResolveHostPath(state.root, guest.relative_path()); + std::error_code error; + if (std::filesystem::exists(supplied, error) || error) return supplied; + auto provided = provide(state.root); + if (provided.empty()) return std::nullopt; + return provided.make_preferred(); +} std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; + if (auto provided = ProvidedPath(state, guest)) return *provided; return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); } void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { @@ -192,7 +213,7 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } -std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false) { +std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; std::filesystem::path input(text); @@ -206,7 +227,7 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool mo for (auto part = relative.begin(); part != relative.end(); ++part) { if (*part == "..") { const auto parent = guest.lexically_normal(); - if (parent.has_relative_path()) RequireHostDirectory(state, parent); + if (parent.has_relative_path() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, parent)))) RequireHostDirectory(state, parent); } guest /= *part; } @@ -354,6 +375,23 @@ extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* h aliases.entries.emplace_back(prefix, hostPath); } +extern "C" void AddPathProvider_nid_no_patch(const char* guestPath, PathProvider provide) { + if (guestPath == nullptr || provide == nullptr) { + APS5_INVALID_ARG_EX; + } + const auto path = TrimSlashes(guestPath); + if (path.empty()) { APS5_INVALID_ARG_EX; } + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + for (auto& entry : aliases.providers) { + if (SameName(entry.first, path)) { + entry.second = provide; + return; + } + } + aliases.providers.emplace_back(path, provide); +} + extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix) { if (guestPrefix == nullptr) { APS5_INVALID_ARG_EX; @@ -424,18 +462,32 @@ extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix) { aliases.blocked.push_back(prefix); } -extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { +static std::filesystem::path ResolveLocked(const char* path, bool module, bool archived) { if (!path) { APS5_INVALID_ARG_EX; } + if (archived) AppArchiveReady_nid_no_patch(); auto& state = Directories(); std::lock_guard lock(state.mutex); - return Resolve(state, path); + return Resolve(state, path, module, archived); +} + +extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path) { + return ResolveLocked(path, false, false); +} + +extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module) { + return ResolveLocked(path, module, true); +} + +extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { + auto resolved = ResolveLocked(path, false, true); + AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); + return resolved; } extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path) { - if (!path) { APS5_INVALID_ARG_EX; } - auto& state = Directories(); - std::lock_guard lock(state.mutex); - return Resolve(state, path, true); + auto resolved = ResolveLocked(path, true, true); + AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); + return resolved; } extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path) { @@ -472,9 +524,11 @@ extern "C" int APS5_VABI chdir_nid_postfix(const char* path) { if (!path) { errno = 14; return -1; } if (!*path) { errno = 2; return -1; } try { + AppArchiveReady_nid_no_patch(); auto& state = Directories(); std::lock_guard lock(state.mutex); - const auto resolved = Resolve(state, path); + const auto resolved = Resolve(state, path, false, true); + AppArchiveRefuseOnly_nid_no_patch(resolved, path, "changing the working directory to"); const int error = SetWorkingDirectory(state, resolved); if (error) { errno = error; return -1; } return 0; @@ -494,8 +548,8 @@ extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) { const auto relative = state.current.lexically_relative(state.root); const auto path = relative == "." ? std::string("/") : "/" + relative.generic_string(); const auto required = path.size() + 1; - if ((buffer || size) && size < required) { errno = 34; return nullptr; } - if (!buffer) buffer = static_cast(ApplicationHeapAllocate_nid_no_patch(size ? size : required)); + if (buffer && size < required) { errno = 34; return nullptr; } + if (!buffer) buffer = static_cast(ApplicationHeapAllocate_nid_no_patch(required)); if (!buffer) { errno = 12; return nullptr; } std::memcpy(buffer, path.c_str(), required); return buffer; diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index d6bc09c0af..438a819c8f 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -95,12 +95,24 @@ std::vector> fileBackedWritableImage() { return mappings; } +#ifdef ANYPS5_ENABLE_ASAN +[[gnu::no_sanitize("address")]] void copyImagePages(void* target, const void* source, std::size_t bytes) { + auto* to = static_cast(target); + const auto* from = static_cast(source); + for (std::size_t word = 0; word < bytes / sizeof(std::uintptr_t); ++word) to[word] = from[word]; +} +#endif + bool backWritableImageAnonymously() { for (const auto& [address, bytes] : fileBackedWritableImage()) { auto* image = reinterpret_cast(address); auto* copy = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (copy == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "mmap of an anonymous main image copy failed"); +#ifdef ANYPS5_ENABLE_ASAN + copyImagePages(copy, image, bytes); +#else std::memcpy(copy, image, bytes); +#endif if (mremap(copy, bytes, bytes, MREMAP_MAYMOVE | MREMAP_FIXED, image) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "mremap of an anonymous main image copy failed"); } return true; @@ -227,43 +239,42 @@ void GuestAllocationsRegisterImage_nid_postfix(void*, const void* image) { registerImage(image); } -void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image) { - auto& state = registry(); - const auto found = state.registeredImages.find(reinterpret_cast(image)); - if (found == state.registeredImages.end()) return; - const auto [begin, end] = *found; - GuestAllocationsRequireUnpinned_nid_postfix(mutation, image, end - begin); - recordChange(mutation, image, end - begin); - std::erase_if(state.ranges, [&](const auto& entry) { return !entry.second->releasable && entry.second->allocationAddress >= begin && entry.second->allocationAddress < end; }); - state.registeredImages.erase(found); -} #else -void GuestAllocationsRegisterMainImage_nid_postfix(void*) { - auto& state = registry(); - if (state.mainImageRegistered) return; +namespace { +struct ImagePages { struct Page { bool readable = false; bool writable = false; }; + std::uintptr_t base = 0; + bool main = false; + bool found = false; + std::uint64_t pageSize = 0; std::map pages; - const auto pageSize = static_cast(::sysconf(_SC_PAGESIZE)); - std::pair*, std::uint64_t> collection{&pages, pageSize}; - dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { - auto& collected = *static_cast*, std::uint64_t>*>(data); - for (int index = 0; index < image->dlpi_phnum; ++index) { - const auto& header = image->dlpi_phdr[index]; - if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; - const auto start = (image->dlpi_addr + header.p_vaddr) & ~(collected.second - 1); - const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + collected.second - 1) & ~(collected.second - 1); - for (auto page = start; page < end; page += collected.second) { - auto& entry = (*collected.first)[page]; - entry.readable = entry.readable || (header.p_flags & (PF_R | PF_W)) != 0; - entry.writable = entry.writable || (header.p_flags & PF_W) != 0; - } +}; + +int collectImagePages(dl_phdr_info* image, std::size_t, void* data) { + auto& collected = *static_cast(data); + if (!collected.main && image->dlpi_addr != collected.base) return 0; + collected.found = true; + for (int index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + const auto start = (image->dlpi_addr + header.p_vaddr) & ~(collected.pageSize - 1); + const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + collected.pageSize - 1) & ~(collected.pageSize - 1); + for (auto page = start; page < end; page += collected.pageSize) { + auto& entry = collected.pages[page]; + entry.readable = entry.readable || (header.p_flags & (PF_R | PF_W)) != 0; + entry.writable = entry.writable || (header.p_flags & PF_W) != 0; } - return 1; - }, &collection); - require(!pages.empty(), "main guest image has no loadable segments"); + } + return 1; +} + +std::uintptr_t registerImagePages(const ImagePages& collected) { + auto& state = registry(); + const auto pageSize = collected.pageSize; + const auto& pages = collected.pages; auto replacement = state.ranges; for (auto page = pages.begin(); page != pages.end();) { auto last = page; @@ -280,10 +291,45 @@ void GuestAllocationsRegisterMainImage_nid_postfix(void*) { page = std::next(last); } state.ranges.swap(replacement); + return static_cast(std::prev(pages.end())->first + pageSize); +} +} + +void GuestAllocationsRegisterMainImage_nid_postfix(void*) { + auto& state = registry(); + if (state.mainImageRegistered) return; + ImagePages collected{.main = true, .pageSize = static_cast(::sysconf(_SC_PAGESIZE))}; + dl_iterate_phdr(collectImagePages, &collected); + require(!collected.pages.empty(), "main guest image has no loadable segments"); + registerImagePages(collected); state.mainImageRegistered = true; } + +void GuestAllocationsRegisterImage_nid_postfix(void*, const void* image) { + require(image != nullptr, "cannot register a guest image without a base"); + auto& state = registry(); + const auto base = reinterpret_cast(image); + if (state.registeredImages.contains(base)) return; + ImagePages collected{.base = base, .pageSize = static_cast(::sysconf(_SC_PAGESIZE))}; + dl_iterate_phdr(collectImagePages, &collected); + require(collected.found, "cannot locate a guest image"); + require(!collected.pages.empty(), "guest image has no loadable segments"); + require(collected.pages.begin()->first == base, "guest image does not start at its base"); + state.registeredImages.emplace(base, registerImagePages(collected)); +} #endif +void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image) { + auto& state = registry(); + const auto found = state.registeredImages.find(reinterpret_cast(image)); + if (found == state.registeredImages.end()) return; + const auto [begin, end] = *found; + GuestAllocationsRequireUnpinned_nid_postfix(mutation, image, end - begin); + recordChange(mutation, image, end - begin); + std::erase_if(state.ranges, [&](const auto& entry) { return !entry.second->releasable && entry.second->allocationAddress >= begin && entry.second->allocationAddress < end; }); + state.registeredImages.erase(found); +} + void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu) { recordChange(mutation, pointer, bytes); const auto address = reinterpret_cast(pointer); diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 959d940c38..e155b3ed21 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -330,9 +330,10 @@ public: } void Register(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; std::vector pages((end - begin) / _page, Written | Fresh); std::vector queried(pages.size(), false); const Lock lock(_lock); @@ -364,9 +365,10 @@ public: } bool Unregister(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return false; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return false; + if (end <= begin) return false; const Lock lock(_lock); return erase(begin, end); } @@ -407,9 +409,10 @@ public: } void ProtectionChanged(std::uintptr_t begin, std::uintptr_t end, int protection) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); forEachPage(begin, end, [&](std::uintptr_t, std::uint8_t& state) { state = static_cast(Written | (state & Fresh) | (protection & ProtectionMask)); @@ -417,9 +420,10 @@ public: } void BeginHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); _hostWrites.emplace_back(begin, end); forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { @@ -429,9 +433,10 @@ public: } void EndHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); const auto found = std::find(_hostWrites.begin(), _hostWrites.end(), std::pair{begin, end}); if (found != _hostWrites.end()) _hostWrites.erase(found); diff --git a/core/libs/prx/libc/src/HostErrno.cpp b/core/libs/prx/libc/src/HostErrno.cpp new file mode 100644 index 0000000000..5e01470dec --- /dev/null +++ b/core/libs/prx/libc/src/HostErrno.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/HostErrno.hpp" + +#include + +namespace { + +constexpr int GuestEio = 5; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEagain = 35; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int GuestEnotempty = 66; +constexpr int GuestEdquot = 69; +constexpr int GuestEoverflow = 84; + +} + +extern "C" int GuestErrnoFromHost_nid_no_patch(int error) { + if (error == EAGAIN) return GuestEagain; +#ifdef EDEADLK + if (error == EDEADLK) return GuestEdeadlk; +#endif + if (error == ENAMETOOLONG) return GuestEnametoolong; + if (error == ENOTEMPTY) return GuestEnotempty; +#ifdef ELOOP + if (error == ELOOP) return GuestEloop; +#endif +#ifdef EDQUOT + if (error == EDQUOT) return GuestEdquot; +#endif +#ifdef EOVERFLOW + if (error == EOVERFLOW) return GuestEoverflow; +#endif + return error > 0 && error <= 34 ? error : GuestEio; +} diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 3391a07053..f61e17fbd4 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -9,10 +9,10 @@ #include #include #endif +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include -#include #include #include #include @@ -213,7 +213,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { std::size_t size = entries.size() * sizeof(GuestInterfaceAddress); for (const auto& entry : entries) size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()); - auto* block = static_cast(std::malloc(size)); + auto* block = static_cast(ApplicationHeapAllocate_nid_no_patch(size)); if (!block) { *__error_nid_postfix() = 12; return -1; @@ -243,7 +243,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { } void APS5_VABI freeifaddrs_nid_postfix(GuestInterfaceAddress* list) { - std::free(list); + ApplicationHeapFree_nid_no_patch(list); } } diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 9d70e10e48..cdc69a38b9 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -157,6 +157,64 @@ std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; +constexpr std::size_t GuestWideCharacterSize = 2; +constexpr std::size_t GuestWideInlineCapacity = sizeof(GuestLocale::String::buffer) / GuestWideCharacterSize - 1; + +void ValidateWideCharacterRange(const std::uint16_t* first, const std::uint16_t* last, const char* function) { + if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); +} + +int APS5_VABI WideCollateCompare(const GuestLocale::CollateFacet*, const char* first1, const char* last1, const char* first2, const char* last2) { + const auto* characters1 = reinterpret_cast(first1); + const auto* end1 = reinterpret_cast(last1); + const auto* characters2 = reinterpret_cast(first2); + const auto* end2 = reinterpret_cast(last2); + ValidateWideCharacterRange(characters1, end1, "collate::do_compare"); + ValidateWideCharacterRange(characters2, end2, "collate::do_compare"); + const auto length1 = static_cast(end1 - characters1); + const auto length2 = static_cast(end2 - characters2); + const auto common = std::min(length1, length2); + for (std::size_t index = 0; index < common; ++index) { + if (characters1[index] != characters2[index]) return characters1[index] < characters2[index] ? -1 : 1; + } + if (length1 == length2) return 0; + return length1 < length2 ? -1 : 1; +} + +GuestLocale::String* APS5_VABI WideCollateTransform(GuestLocale::String* result, const GuestLocale::CollateFacet*, const char* first, const char* last) { + if (result == nullptr) throw std::invalid_argument("collate::do_transform: null result"); + const auto* characters = reinterpret_cast(first); + const auto* end = reinterpret_cast(last); + ValidateWideCharacterRange(characters, end, "collate::do_transform"); + const auto length = static_cast(end - characters); + if (length != 0 && std::find(characters, end, static_cast(0)) != end) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); + const auto bytes = length * GuestWideCharacterSize; + char* data = result->buffer; + std::size_t capacity = GuestWideInlineCapacity; + if (length > GuestWideInlineCapacity) { + capacity = length | GuestWideInlineCapacity; + data = static_cast(AllocateStringStorage((capacity + 1) * GuestWideCharacterSize)); + } + if (length != 0) std::memcpy(data, characters, bytes); + data[bytes] = 0; + data[bytes + 1] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +std::int64_t APS5_VABI WideCollateHash(const GuestLocale::CollateFacet*, const char* first, const char* last) { + const auto* characters = reinterpret_cast(first); + const auto* end = reinterpret_cast(last); + ValidateWideCharacterRange(characters, end, "collate::do_hash"); + std::uint64_t hash = 0xcbf29ce484222325; + for (; characters != end; ++characters) hash = (hash ^ static_cast(*characters)) * 0x100000001b3; + return static_cast(hash); +} + +const GuestLocale::CollateVtable g_wideCollateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, WideCollateCompare, WideCollateTransform, WideCollateHash}; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -271,8 +329,25 @@ GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_post return &g_classicLocale; } -void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +namespace { +struct GuestLocaleFacade { + GuestLocale::Implementation* implementation; +}; +GuestLocaleFacade g_classicLocaleFacade{&g_classicLocale}; +} + +GuestLocaleFacade* APS5_VABI _ZNSt6locale7classicEv_nid_postfix() { + return &g_classicLocaleFacade; +} + +std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { + if (facet != nullptr && *facet == nullptr) { + if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); + auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); + *collate = {{&g_wideCollateVtable.facet, 0, 0}, nullptr, nullptr}; + *facet = &collate->base; + } + return 1; } std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ac4d5612d3..029440fc53 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -165,6 +165,28 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +template +T GuestFmin(T x, T 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; +} + +template +T GuestFmax(T x, T y) { + if (std::isnan(x)) + return y; + if (std::isnan(y)) + return x; + if (std::signbit(x) != std::signbit(y)) + return std::signbit(x) ? y : x; + return x < y ? y : x; +} + } extern "C" { @@ -256,6 +278,12 @@ 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); } +double APS5_VABI fmin_nid_postfix(double x, double y) { return GuestFmin(x, y); } +float APS5_VABI fminf_nid_postfix(float x, float y) { return GuestFmin(x, y); } +long double APS5_VABI fminl_nid_postfix(long double x, long double y) { return GuestFmin(x, y); } +double APS5_VABI fmax_nid_postfix(double x, double y) { return GuestFmax(x, y); } +float APS5_VABI fmaxf_nid_postfix(float x, float y) { return GuestFmax(x, y); } +long double APS5_VABI fmaxl_nid_postfix(long double x, long double y) { return GuestFmax(x, y); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } long double APS5_VABI asinl_nid_postfix(long double x) { @@ -302,7 +330,16 @@ 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); } +int signgam_nid_postfix = 0; + +double APS5_VABI lgamma_nid_postfix(double x) { + const double whole = std::floor(x); + if (x == 0) signgam_nid_postfix = std::signbit(x) ? -1 : 1; + else signgam_nid_postfix = x < 0 && whole != x && std::fmod(whole, 2.0) != 0 ? -1 : 1; + const std::uint64_t magnitude = std::bit_cast(x) & 0x7fffffffffffffffull; + if (magnitude != 0 && magnitude < 0x3c70000000000000ull) return -std::log(std::fabs(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); } @@ -500,6 +537,13 @@ int APS5_VABI __signbitl_nid_postfix(long double x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __isfinitel_nid_postfix(long double x) { + static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64 && std::numeric_limits::max_exponent == 16384 && std::endian::native == std::endian::little, "guest long double requires little-endian x87 extended precision storage"); + std::uint16_t exponent; + std::memcpy(&exponent, reinterpret_cast(&x) + 8, sizeof(exponent)); + return (exponent & 0x7fffu) != 0x7fffu; +} + static std::mutex g_randLock; static std::uint32_t g_randState = 1; diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index d4bca7d3f2..e6d58a919f 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -52,7 +53,7 @@ extern "C" void LibcRequestShutdown_nid_postfix() { extern "C" void LibcRequestExit_nid_postfix(int code) { if (exitRequested.exchange(true)) return; LibcRequestShutdown_nid_postfix(); - std::thread([code] { LibcExit_nid_no_patch(code); }).detach(); + std::thread([code] { LibcExitWithoutHandlers_nid_no_patch(code); }).detach(); } extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix() { @@ -98,13 +99,44 @@ extern "C" void LibcRunShutdown_nid_postfix() { namespace { +std::atomic endFlush{nullptr}; +void (*previousAbortHandler)(int) = SIG_DFL; + +void FlushOnAbort(int number) { + LibcRunEndFlush_nid_no_patch(); + if (previousAbortHandler == SIG_IGN) return; + if (previousAbortHandler != SIG_DFL && previousAbortHandler != SIG_ERR) { + previousAbortHandler(number); + return; + } +#ifndef _WIN32 + std::signal(number, SIG_DFL); + std::raise(number); +#endif +} + +} + +extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()) { + if (flush == nullptr) throw std::runtime_error("libc: invalid end flush registration"); + void (*registered)() = nullptr; + if (!endFlush.compare_exchange_strong(registered, flush) && registered != flush) throw std::runtime_error("libc: end flush already registered"); + static std::once_flag installed; + std::call_once(installed, [] { previousAbortHandler = std::signal(SIGABRT, FlushOnAbort); }); +} + +extern "C" void LibcRunEndFlush_nid_no_patch() { + if (const auto flush = endFlush.load(std::memory_order_acquire)) flush(); +} + +namespace { + void StartExit() { exitRequested.store(true); if (exitStarted.exchange(true)) { if (!runningExit) LibcAwaitExit_nid_postfix(); } else { runningExit = true; - LibcRunShutdown_nid_postfix(); } } @@ -122,20 +154,27 @@ extern "C" { } [[noreturn]] void LibcTerminate_nid_no_patch(int code) { + LibcRunEndFlush_nid_no_patch(); #ifdef _WIN32 TerminateProcess(GetCurrentProcess(), static_cast(code)); #endif std::_Exit(code); } +void LibcStartExit_nid_postfix() { + StartExit(); +} + [[noreturn]] void LibcExit_nid_no_patch(int code) { StartExit(); CxaFinalize_nid_no_patch(nullptr); + LibcRunShutdown_nid_postfix(); std::exit(code); } [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code) { StartExit(); + LibcRunShutdown_nid_postfix(); std::fflush(nullptr); LibcTerminate_nid_no_patch(code); } @@ -159,6 +198,7 @@ void APS5_VABI exit_nid_postfix(int code) { ) { (void)arg0; (void)arg1; (void)arg2; (void)arg3; (void)arg4; (void)arg5; + LibcRunEndFlush_nid_no_patch(); std::fprintf(stderr, "[libc] abort() called from %p\n", __builtin_return_address(0)); std::fflush(nullptr); std::abort(); diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 4b7ae591d7..013183fc95 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -192,6 +192,10 @@ unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, un return SonyUmodti3(dividend, divisor); } +__int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyModti3(dividend, divisor); +} + void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void*) { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 69e24de20c..8d136d05b4 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -15,14 +15,40 @@ FileStream* __stderrp_nid_postfix = &_Stderr_nid_postfix; int __isthreaded_nid_postfix = 1; int APS5_VABI fgetc_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fgetc(GetNativeStream(stream)); stream->SyncStatus(); return result; } std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { - return fgetc_nid_postfix(stream); + if (stream == nullptr) { + errno = 22; + return -1; + } + const int first = fgetc_nid_postfix(stream); + if (first == -1) return -1; + const unsigned char lead = static_cast(first); + if (lead < 0x80) return lead; + const std::size_t extra = lead >= 0xf0 ? 3 : lead >= 0xe0 ? 2 : lead >= 0xc0 ? 1 : 0; + if (extra == 0) { + errno = 86; + stream->SetError(); + return -1; + } + std::uint32_t code = extra == 3 ? (lead & 0x07) : extra == 2 ? (lead & 0x0f) : (lead & 0x1f); + for (std::size_t index = 0; index < extra; ++index) { + const int next = fgetc_nid_postfix(stream); + if (next == -1 || (static_cast(next) & 0xc0) != 0x80) { + errno = 86; + stream->SetError(); + return -1; + } + code = (code << 6) | (static_cast(next) & 0x3f); + } + return static_cast(code); } + std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* stream) { if (value == -1) return -1; if (value < 0 || value > 255) { @@ -36,6 +62,7 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea } int APS5_VABI getc_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int result = _fgetc_nolock(GetNativeStream(stream)); #else @@ -46,13 +73,18 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { } int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getchar_nid_postfix() { return fgetc_nid_postfix(__stdinp_nid_postfix); } +std::int32_t APS5_VABI getwc_nid_postfix(FileStream* stream) { return fgetwc_nid_postfix(stream); } +std::int32_t APS5_VABI getwchar_nid_postfix() { return fgetwc_nid_postfix(__stdinp_nid_postfix); } +int APS5_VABI getchar_unlocked_nid_postfix() { return getc_unlocked_nid_postfix(__stdinp_nid_postfix); } int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fputc(value, GetNativeStream(stream)); stream->SyncStatus(); return result; } int APS5_VABI putc_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int result = _fputc_nolock(value, GetNativeStream(stream)); #else @@ -63,12 +95,15 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { } int APS5_VABI __swbuf_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, __stdoutp_nid_postfix); } +int APS5_VABI putchar_unlocked_nid_postfix(int value) { return putc_unlocked_nid_postfix(value, __stdoutp_nid_postfix); } int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::ungetc(value, GetNativeStream(stream)); stream->SyncStatus(); return result; } char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return nullptr; } if (size <= 0) { stream->SetError(); errno = 22; @@ -90,6 +125,7 @@ void APS5_VABI clearerr_nid_postfix(FileStream* stream) { stream->ClearError(); } int APS5_VABI fileno_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int descriptor = _fileno(GetNativeStream(stream)); #else @@ -99,6 +135,7 @@ int APS5_VABI fileno_nid_postfix(FileStream* stream) { return descriptor; } int APS5_VABI setvbuf_nid_postfix(FileStream* stream, char* buffer, int mode, std::size_t size) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (mode < 0 || mode > 2) return -1; const int native = mode == 0 ? _IOFBF : mode == 1 ? _IOLBF : _IONBF; if (native != _IONBF && size == 0) { diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index ab69378356..28b5f7b33b 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -38,6 +38,24 @@ void* Allocate(std::size_t size) { } } +void* AllocateAligned(std::size_t size, std::size_t alignment) { + for (;;) { + GuestNewHandler handler = nullptr; + try { + return ApplicationHeapAlign_nid_no_patch(alignment, size == 0 ? 1 : size); + } catch (const std::bad_alloc&) { + handler = g_newHandler.load(); + if (handler == nullptr) throw; + } catch (const std::length_error&) { + handler = g_newHandler.load(); + if (handler == nullptr) throw std::bad_alloc(); + } catch (const std::invalid_argument&) { + throw std::bad_alloc(); + } + handler(); + } +} + } extern "C" { @@ -89,6 +107,10 @@ void* APS5_VABI _Znam_nid_postfix(std::size_t size) { return Allocate(size); } +void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t size, std::size_t alignment) { + return AllocateAligned(size, alignment); +} + void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { try { return Allocate(size); diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index b1d85f9faf..a5cc181812 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -246,7 +246,8 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan 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)); + std::chrono::seconds(deadline.tv_sec) + + std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); } catch (const std::system_error&) { lock.release(); diff --git a/core/libs/prx/libc/src/WideConversion.cpp b/core/libs/prx/libc/src/WideConversion.cpp index 9b7c52d1ab..09fb1fd1c9 100644 --- a/core/libs/prx/libc/src/WideConversion.cpp +++ b/core/libs/prx/libc/src/WideConversion.cpp @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include extern "C" void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error) { SonyIgnoreHandlerS(message, pointer, error); @@ -32,3 +35,51 @@ extern "C" std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const s extern "C" int APS5_VABI wcscpy_s_nid_postfix(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { return SonyWcscpyS(dest, size, src); } + +extern "C" int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t capacity); + +namespace { + +std::size_t ReadErrorMessage(int error, char (&message)[128]) { + const int result = strerror_r_nid_postfix(error, message, sizeof(message)); + if (result != 0 && result != 22) throw std::runtime_error("strerror_s: error message exceeds internal buffer"); + return std::strlen(message); +} + +void ErrorMessageConstraint(char* buffer) { + SonyConstraintHandler handler; + { + std::lock_guard lock(SonyConstraintMutex()); + handler = SonyConstraintHandlerSlot(); + } + if (handler == SonyDefaultConstraintHandler) { + SonyDefaultConstraintHandler("strerror_s", buffer, 22); + } else { + using GuestHandler = void (APS5_VABI *)(const char*, void*, int); + reinterpret_cast(handler)("strerror_s", buffer, 22); + } +} + +} + +extern "C" std::size_t APS5_VABI strerrorlen_s_nid_postfix(int error) { + char message[128]; + return ReadErrorMessage(error, message); +} + +extern "C" int APS5_VABI strerror_s_nid_postfix(char* buffer, std::size_t capacity, int error) { + if (!buffer || capacity == 0 || capacity > (std::numeric_limits::max() >> 1)) { + ErrorMessageConstraint(buffer); + return 22; + } + char message[128]; + const auto length = ReadErrorMessage(error, message); + if (length < capacity) { + std::memcpy(buffer, message, length + 1); + return 0; + } + std::memcpy(buffer, message, capacity - 1); + buffer[capacity - 1] = '\0'; + if (capacity > 3) std::memcpy(buffer + capacity - 4, "...", 3); + return 34; +} diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index dbc0c7acbc..231c5e0679 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -13,13 +13,6 @@ extern "C" [[noreturn]] GCC_HIDDEN_FN void APS5_VABI LibcRuntimeAssertion(const char*, int, const char*, const char*) { std::abort(); } GCC_LOCAL_ALIAS(_ZSt21__glibcxx_assert_failPKciS0_S0_, LibcRuntimeAssertion); -GCC_GLOBAL_ALIAS(__cxa_allocate_exception_nid_no_patch_cut, __cxa_allocate_exception_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_throw_nid_no_patch_cut, __cxa_throw_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_begin_catch_nid_no_patch_cut, __cxa_begin_catch_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_end_catch_nid_no_patch_cut, __cxa_end_catch_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_rethrow_nid_no_patch_cut, __cxa_rethrow_nid_postfix); -GCC_GLOBAL_ALIAS(__gxx_personality_v0_nid_no_patch_cut, __gxx_personality_v0_nid_postfix); -GCC_GLOBAL_ALIAS(_Unwind_Resume_nid_no_patch_cut, _Unwind_Resume_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_free_exception, __cxa_free_exception_nid_postfix); GCC_GLOBAL_ALIAS(_ZSt9terminatev, _ZSt9terminatev_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_call_terminate, __cxa_call_terminate_nid_postfix); diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 0ccea4f8ad..4f96164637 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -131,9 +131,29 @@ bool Equal(const std::type_info* a, const std::type_info* b) { return a == b || (a && b && std::strcmp(a->name(), b->name()) == 0); } -const char* Kind(const std::type_info* type) { +static const std::type_info* TypeOfTypeInfo(const std::type_info* type) { auto table = *reinterpret_cast(type); - return table[-1]->name(); + return table[-1]; +} + +static const std::type_info* BaseAtSameAddress(const std::type_info* type) { + const char* kind = TypeOfTypeInfo(type)->name(); + if (std::strcmp(kind, "N10__cxxabiv120__si_class_type_infoE") == 0) + return static_cast(type)->__base_type; + if (std::strcmp(kind, "N10__cxxabiv121__vmi_class_type_infoE") != 0) return nullptr; + auto* vmi = static_cast(type); + if (vmi->__base_count != 1 || (vmi->__base_info[0].__offset_flags & ~2L) != 0) return nullptr; + return vmi->__base_info[0].__base_type; +} + +const char* Kind(const std::type_info* type) { + const std::type_info* kind = TypeOfTypeInfo(type); + for (unsigned depth = 0; depth < 16 && std::strncmp(kind->name(), "N10__cxxabiv1", 13) != 0; ++depth) { + const std::type_info* base = BaseAtSameAddress(kind); + if (!base) break; + kind = base; + } + return kind->name(); } struct Search { diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index fdaca18500..61d5ee879a 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -344,6 +344,10 @@ void APS5_VABI _ZNSt11logic_errorC2EPKc_nid_postfix(LibcException::ExceptionObje void APS5_VABI _ZNSt11logic_errorC1ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt11logic_error_nid_postfix); } void APS5_VABI _ZNSt11logic_errorC2ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt11logic_error_nid_postfix); } LibcException::ExceptionObject* APS5_VABI _ZNSt11logic_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } +[[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const LibcException::ExceptionObject* self) { + if (!self) throw std::invalid_argument("exception object is null"); + LibcException::ThrowMessage(_ZTVSt11logic_error_nid_postfix, LibcException::MessageWhat(self)); +} LibcException::TypeRecord _ZTISt13runtime_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St13runtime_error", &_ZTISt9exception_nid_postfix}; LibcException::ExceptionVtable _ZTVSt13runtime_error_nid_postfix {0, &_ZTISt13runtime_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; @@ -459,6 +463,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep [[noreturn]] void APS5_VABI _ZSt14_Xout_of_rangePKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt12out_of_range_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt14_Xlength_errorPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt12length_error_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt18_Xinvalid_argumentPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt16invalid_argument_nid_postfix, message); } +[[noreturn]] void APS5_VABI _ZSt15_Xruntime_errorPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt13runtime_error_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt13_Xrange_errorPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt11range_error_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt19_Xbad_function_callv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt17bad_function_call_nid_postfix); } [[noreturn]] void APS5_VABI _ZNSt8__sce_v219_Xbad_function_callEv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt17bad_function_call_nid_postfix); } diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 6491da364c..47cfaed9fc 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -2,7 +2,10 @@ #include #include #include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/FailureReport.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/CrashText.hpp" #include #include #include @@ -304,6 +307,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { // Another thread is already reporting; let it finish before this fault ends the process. Sleep(INFINITE); } + LibcRunEndFlush_nid_no_patch(); const auto* context = info->ContextRecord; char line[MAX_PATH + 64]; char threadName[128] = ""; @@ -352,6 +356,15 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } } + { + const std::pair registers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"rbp", context->Rbp}, {"r8", context->R8}, {"r9", context->R9}, {"r10", context->R10}, {"r11", context->R11}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}}; + static std::uint8_t bytes[crash_text::MaximumBytes]; + static char text[960]; + for (const auto& [name, value] : registers) { + if (value < 0x10000) continue; + if (crash_text::Format(bytes, ReadCode(value, bytes, sizeof(bytes)), text, sizeof(text))) Report(" [%s] %s\n", name, text); + } + } Report(" frame pointer chain:\n"); std::uint64_t frame = context->Rbp; for (int depth = 0; depth < 32 && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8); ++depth) { @@ -394,6 +407,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { ReportCallTrace(); std::fflush(stderr); ReportAllThreads(); + LibcRunFailureReports_nid_no_patch(); std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } @@ -546,10 +560,12 @@ void ReportBacktrace(const char* what, bool reportThreads) { void AbortSignalHandler(int) { ReportBacktrace("abort() (SIGABRT)", true); + LibcRunFailureReports_nid_no_patch(); } void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { ReportBacktrace("UCRT invalid parameter", true); + LibcRunFailureReports_nid_no_patch(); } // Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH), diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index c16167088f..8f7367c074 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -10,6 +10,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -62,6 +63,11 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int errno = EFAULT; return -1; } + if (const auto archived = File::ReadArchivedFile(fd, buf, nbyte, &offset)) { + if (*archived >= 0) return *archived; + errno = static_cast(-*archived); + return -1; + } #ifdef _WIN32 if (offset < 0) { errno = EINVAL; diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index e5f172fcea..c2c9b2d2c6 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include namespace { +std::mutex g_metadataMutex; bool g_titleLoaded = false; bool g_iconAttempted = false; bool g_iconLoaded = false; @@ -32,6 +34,7 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std } void ensureTitleLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); @@ -68,6 +71,7 @@ const bool g_dataMounted = [] { }(); void ensureIconLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_iconAttempted) return; g_iconAttempted = true; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataIconGuestPath); @@ -76,10 +80,11 @@ void ensureIconLoaded() { if (!file.is_open()) throw std::runtime_error("failed to open icon0.png"); file.seekg(0, std::ios::end); const auto fileSize = file.tellg(); + if (file.fail() || fileSize <= 0) throw std::runtime_error("icon0.png is empty or unreadable"); file.seekg(0, std::ios::beg); std::vector bytes(static_cast(fileSize)); - if (!bytes.empty()) file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - if (bytes.empty()) throw std::runtime_error("icon0.png is empty"); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file || file.gcount() != static_cast(bytes.size())) throw std::runtime_error("failed to read icon0.png"); constexpr std::uint8_t PngSignature[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; if (bytes.size() < 8 || std::memcmp(bytes.data(), PngSignature, sizeof(PngSignature)) != 0) throw std::runtime_error("icon0.png is not a valid PNG file"); g_iconBytes = std::move(bytes); diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 8cde56681f..e8ac7d212a 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -337,9 +337,9 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { const auto parsed = std::from_chars(number.data(), number.data() + number.size(), bytes); if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds uint64 range"); - if (bytes == 0 || (bytes & (PS5_PAGE_SIZE - 1)) != 0) - throw std::runtime_error("param.json kernel.flexibleMemorySize must be a positive multiple of 16 KiB"); - result.flexibleMemorySizeBytes = bytes; + if ((bytes & (PS5_PAGE_SIZE - 1)) != 0) + throw std::runtime_error("param.json kernel.flexibleMemorySize must be a multiple of 16 KiB"); + if (bytes != 0) result.flexibleMemorySizeBytes = bytes; } } if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8611b6f0a2..5ea617fe76 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/ArchivedFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/FileEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/Kqueue.cpp @@ -15,6 +16,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SharedMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/RandomDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/HostStreams.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SandboxCertificates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -47,6 +49,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketConnect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/DynamicLoader.cpp @@ -80,7 +83,9 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) - target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 wbemuuid ole32 oleaut32) + target_sources(libkernel PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/File/src/TrustedRoots.cpp) + target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 crypt32 wbemuuid ole32 oleaut32) + target_link_libraries(libkernel PRIVATE iphlpapi) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) @@ -224,7 +229,7 @@ if(BUILD_TESTING) add_dependencies(guest_dynamic_loader_fixed_base_tests guest_module_fixture) set_target_properties(guest_dynamic_loader_fixed_base_tests PROPERTIES ENABLE_EXPORTS ON POSITION_INDEPENDENT_CODE OFF) target_compile_options(guest_dynamic_loader_fixed_base_tests PRIVATE -fno-pie) - target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie) + target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie -fuse-ld=bfd) add_test(NAME guest_dynamic_loader_fixed_base COMMAND guest_dynamic_loader_fixed_base_tests $) set_tests_properties(guest_dynamic_loader_fixed_base PROPERTIES WORKING_DIRECTORY "$") @@ -260,6 +265,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() if(UNIX) + add_test_executable(guest_record_lock_owners_tests ${LIBS_INCLUDE_DIR}/tests/GuestRecordLockOwners.cpp) + target_include_directories(guest_record_lock_owners_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_record_lock_owners_tests PRIVATE libkernel libc) + add_test(NAME guest_record_lock_owners COMMAND guest_record_lock_owners_tests) + set_tests_properties(guest_record_lock_owners PROPERTIES TIMEOUT 20) add_test_executable(guest_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) @@ -295,6 +305,25 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sync PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sandbox_certificates_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxCertificates.cpp) + target_include_directories(guest_sandbox_certificates_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sandbox_certificates_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sandbox_certificates_tests) + set(sandboxCertificatesRun ${CMAKE_CURRENT_BINARY_DIR}/guest_sandbox_certificates) + file(MAKE_DIRECTORY ${sandboxCertificatesRun}) + add_test(NAME guest_sandbox_certificates COMMAND guest_sandbox_certificates_tests WORKING_DIRECTORY ${sandboxCertificatesRun}) + set_tests_properties(guest_sandbox_certificates PROPERTIES TIMEOUT 20) + if(WIN32) + add_test_executable(guest_sandbox_certificate_trust_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxCertificateTrust.cpp) + target_include_directories(guest_sandbox_certificate_trust_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sandbox_certificate_trust_tests PRIVATE libkernel libc crypt32) + configure_windows_unwind(guest_sandbox_certificate_trust_tests) + add_test(NAME guest_sandbox_certificate_trust COMMAND guest_sandbox_certificate_trust_tests) + set_tests_properties(guest_sandbox_certificate_trust PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_sandbox_certificates PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_rename_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelRename.cpp) target_include_directories(guest_kernel_rename_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_rename_tests PRIVATE libkernel libc) @@ -352,10 +381,21 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_is_stack_tests) add_test(NAME guest_kernel_is_stack COMMAND guest_kernel_is_stack_tests) set_tests_properties(guest_kernel_is_stack PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_kernel_is_stack detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_app_icon_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppIcon.cpp) + target_include_directories(guest_app_icon_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_icon_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_app_icon_tests) + add_test(NAME guest_app_icon COMMAND guest_app_icon_tests) + set_tests_properties(guest_app_icon PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_icon PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_app_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppInfo.cpp) target_include_directories(guest_app_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_app_info_tests PRIVATE libkernel libc) @@ -372,6 +412,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_stack_protect_tests) add_test(NAME guest_kernel_stack_protect COMMAND guest_kernel_stack_protect_tests) set_tests_properties(guest_kernel_stack_protect PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_kernel_stack_protect detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_kernel_stack_protect PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -382,6 +423,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_thread_self_tests) add_test(NAME guest_thread_self COMMAND guest_thread_self_tests) set_tests_properties(guest_thread_self PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_thread_self detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -426,6 +468,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_schedparam PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_attr_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrStack.cpp) + target_include_directories(guest_pthread_attr_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_attr_stack_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_attr_stack_tests) + add_test(NAME guest_pthread_attr_stack COMMAND guest_pthread_attr_stack_tests) + set_tests_properties(guest_pthread_attr_stack PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_attr_stack 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) @@ -472,6 +524,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_pthread_stack_tests) add_test(NAME guest_pthread_stack COMMAND guest_pthread_stack_tests) set_tests_properties(guest_pthread_stack PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_pthread_stack detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_pthread_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -486,6 +539,16 @@ if(BUILD_TESTING) set_tests_properties(guest_cond_timedwait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_condattr_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCondattr.cpp) + target_include_directories(guest_pthread_condattr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_condattr_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_condattr_tests) + add_test(NAME guest_pthread_condattr COMMAND guest_pthread_condattr_tests) + set_tests_properties(guest_pthread_condattr PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_condattr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_cond_recursive_mutex_tests ${LIBS_INCLUDE_DIR}/tests/GuestCondRecursiveMutex.cpp) target_include_directories(guest_cond_recursive_mutex_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cond_recursive_mutex_tests PRIVATE libkernel libc) @@ -565,6 +628,9 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_nanosleep PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME guest_nanosleep_timer_self_test COMMAND guest_nanosleep_tests) + set_tests_properties(guest_nanosleep_timer_self_test PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_TRACE_TIMERS=1" ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -608,6 +674,16 @@ if(BUILD_TESTING) set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_timed_locks_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadTimedLocks.cpp) + target_include_directories(guest_pthread_timed_locks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_timed_locks_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_timed_locks_tests) + add_test(NAME guest_pthread_timed_locks COMMAND guest_pthread_timed_locks_tests) + set_tests_properties(guest_pthread_timed_locks PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_timed_locks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_rwlock_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadRwlock.cpp) target_include_directories(guest_pthread_rwlock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_rwlock_tests PRIVATE libkernel libc) @@ -628,6 +704,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_join_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_second_join_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSecondJoin.cpp) + target_include_directories(guest_pthread_second_join_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_second_join_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_second_join_tests) + add_test(NAME guest_pthread_second_join COMMAND guest_pthread_second_join_tests) + set_tests_properties(guest_pthread_second_join PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_second_join PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_key_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadKey.cpp) target_include_directories(guest_pthread_key_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_key_tests PRIVATE libkernel libc) @@ -749,6 +835,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_raise_exception_tests) add_test(NAME guest_raise_exception COMMAND guest_raise_exception_tests) set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 30) + add_asan_test_options(guest_raise_exception detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_raise_exception PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -939,6 +1026,16 @@ if(BUILD_TESTING) set_tests_properties(guest_getrlimit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sysarch_tests ${LIBS_INCLUDE_DIR}/tests/GuestSysarch.cpp) + target_include_directories(guest_sysarch_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sysarch_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sysarch_tests) + add_test(NAME guest_sysarch COMMAND guest_sysarch_tests) + set_tests_properties(guest_sysarch PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sysarch 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) @@ -1078,6 +1175,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_stream_options PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_timeouts_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketTimeouts.cpp) + target_include_directories(guest_socket_timeouts_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_timeouts_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_timeouts_tests) + add_test(NAME guest_socket_timeouts COMMAND guest_socket_timeouts_tests) + set_tests_properties(guest_socket_timeouts PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_timeouts PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) @@ -1149,6 +1256,18 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_pair PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelAppArchive.cpp) + target_include_directories(guest_kernel_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kernel_app_archive_tests PRIVATE libkernel libc zarchive) + configure_windows_unwind(guest_kernel_app_archive_tests) + foreach(mode archive plain mmap) + add_test(NAME guest_kernel_app_archive_${mode} COMMAND guest_kernel_app_archive_tests ${mode}) + set_tests_properties(guest_kernel_app_archive_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_app_archive_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_dup_tests ${LIBS_INCLUDE_DIR}/tests/GuestDup.cpp) target_include_directories(guest_dup_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dup_tests PRIVATE libkernel libc) @@ -1200,6 +1319,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_events PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_connect_listening_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketConnectListening.cpp) + target_include_directories(guest_socket_connect_listening_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_connect_listening_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_connect_listening_tests) + add_test(NAME guest_socket_connect_listening COMMAND guest_socket_connect_listening_tests) + set_tests_properties(guest_socket_connect_listening PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_connect_listening PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_elf_phdr_match_tests ${LIBS_INCLUDE_DIR}/tests/GuestElfPhdrMatch.cpp) target_include_directories(guest_elf_phdr_match_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_elf_phdr_match_tests PRIVATE libkernel libc) @@ -1353,6 +1482,16 @@ if(BUILD_TESTING) if(MINGW) target_compile_options(guest_directory_close_tests PRIVATE -fasynchronous-unwind-tables) endif() + add_test_executable(guest_positioned_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestPositionedClose.cpp) + target_include_directories(guest_positioned_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_positioned_close_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_positioned_close_tests) + add_test(NAME guest_positioned_close COMMAND guest_positioned_close_tests) + set_tests_properties(guest_positioned_close PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + if(MINGW) + target_compile_options(guest_positioned_close_tests PRIVATE -fasynchronous-unwind-tables) + endif() endif() add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -1363,6 +1502,16 @@ if(BUILD_TESTING) set_tests_properties(guest_file_vector PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fstat_fault_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatFault.cpp) + target_include_directories(guest_fstat_fault_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fstat_fault_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_fstat_fault_tests) + add_test(NAME guest_fstat_fault COMMAND guest_fstat_fault_tests) + set_tests_properties(guest_fstat_fault PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_fstat_fault PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) target_include_directories(guest_fstatfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstatfs_tests PRIVATE libkernel libc) @@ -1379,8 +1528,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_fcntl_tests) add_test(NAME guest_kernel_fcntl COMMAND guest_kernel_fcntl_tests) set_tests_properties(guest_kernel_fcntl PROPERTIES TIMEOUT 15) + add_test_executable(guest_append_writes_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppendWrites.cpp) + target_include_directories(guest_append_writes_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_append_writes_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_append_writes_tests) + add_test(NAME guest_append_writes COMMAND guest_append_writes_tests) + set_tests_properties(guest_append_writes PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_kernel_fcntl PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_fcntl guest_append_writes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test_executable(guest_file_lock_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileLock.cpp) target_include_directories(guest_file_lock_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 36f91b26f0..4ceb6dae70 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -23,6 +23,7 @@ #if defined(__linux__) #include +#include #include #include #include @@ -57,6 +58,7 @@ struct KernelArena { return arena; } bool Contains(const void* pointer, size_t len) const { return GuestArena::GuestArenaContains_nid_postfix(pointer, len); } + bool Owns(const void* pointer, size_t len) const { return Contains(pointer, len) && !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(reinterpret_cast(pointer), len); } void* Allocate(size_t len, size_t alignment) { return GuestArena::GuestArenaAllocate_nid_postfix(len, alignment); } void MarkUsed(const void* pointer, size_t len) { GuestArena::GuestArenaMarkUsed_nid_postfix(pointer, len); } void Release(const void* pointer, size_t len) { GuestArena::GuestArenaRelease_nid_postfix(pointer, len); } @@ -117,9 +119,23 @@ static int munmap_release(void* addr) { return 0; } +static void CommitReservedPages(void* addr, size_t len) { + auto cursor = reinterpret_cast(addr); + const auto end = cursor + len; + while (cursor < end) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualQuery failed"); + const auto regionEnd = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + if (memory.State == MEM_RESERVE && !VirtualAlloc(reinterpret_cast(cursor), regionEnd - cursor, MEM_COMMIT, PAGE_READWRITE)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc stack commit failed"); + cursor = regionEnd; + } +} + static int mprotect(void* addr, size_t len, int prot) { GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(addr), len, WinProtFromPosix(prot)); - if (prot != PROT_NONE && KernelArena::Get().Contains(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); + if (prot != PROT_NONE && KernelArena::Get().Owns(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); auto cursor = reinterpret_cast(addr); const auto end = cursor + len; while (cursor < end) { @@ -1026,6 +1042,35 @@ static bool IsGuestModuleImage(const void* base) { } return false; } +#else +struct GuestImageSearch { + std::uintptr_t address = 0; + std::uintptr_t base = 0; + std::uintptr_t pageSize = 0; + bool guestModule = false; + bool first = true; + bool main = false; + bool found = false; +}; + +static int FindImageContaining(dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + const bool main = search.first; + search.first = false; + for (int index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type != PT_LOAD) continue; + const auto start = (image->dlpi_addr + header.p_vaddr) & ~(search.pageSize - 1); + const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + search.pageSize - 1) & ~(search.pageSize - 1); + if (search.address < start || search.address >= end) continue; + search.base = image->dlpi_addr; + search.guestModule = image->dlpi_name != nullptr && std::string_view(image->dlpi_name).ends_with(".guest.prx"); + search.main = main; + search.found = true; + return 1; + } + return 0; +} #endif int DoMprotect(const void* addr, size_t len, int prot) { @@ -1050,6 +1095,9 @@ int DoMprotect(const void* addr, size_t len, int prot) { if (inside >= insideEnd) return 0; auto* insidePointer = reinterpret_cast(inside); const auto insideBytes = static_cast(insideEnd - inside); +#ifdef _WIN32 + if (nativeProtection != PROT_NONE && !KernelArena::Get().Owns(insidePointer, insideBytes)) CommitReservedPages(insidePointer, insideBytes); +#endif if (mprotect(insidePointer, insideBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); NoteGuestStackProtection(inside, insideBytes, (prot & 3) == 3); RecordProtection(insidePointer, insideBytes, prot); @@ -1066,6 +1114,12 @@ int DoMprotect(const void* addr, size_t len, int prot) { } #else mutation.RegisterMainImage(); + GuestImageSearch image{.address = address, .pageSize = static_cast(::sysconf(_SC_PAGESIZE))}; + dl_iterate_phdr(FindImageContaining, &image); + if (image.found && !image.main) { + if (!image.guestModule) throw std::invalid_argument("Memory protection of a foreign image is not supported"); + mutation.RegisterImage(reinterpret_cast(image.base)); + } #endif std::vector> spans; mutation.ForRegistered(pointer, bytes, [&](const void* span, std::size_t spanBytes) { spans.emplace_back(span, spanBytes); }); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index dca35ab051..8533a87c32 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -134,6 +134,38 @@ void _releaseFlexible(uintptr_t start, size_t len) { } } +std::mutex g_lockedRangesLock; +std::map g_lockedRanges; + +void RecordLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + auto it = g_lockedRanges.begin(); + while (it != g_lockedRanges.end()) { + if (it->first <= end && it->first + it->second >= start) { + start = std::min(start, it->first); + end = std::max(end, it->first + it->second); + it = g_lockedRanges.erase(it); + } else { + ++it; + } + } + g_lockedRanges[start] = end - start; +} + +void ForgetLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + auto it = g_lockedRanges.upper_bound(start); + if (it != g_lockedRanges.begin()) --it; + while (it != g_lockedRanges.end() && it->first < end) { + const uintptr_t rangeStart = it->first; + const uintptr_t rangeEnd = rangeStart + it->second; + if (rangeEnd <= start) { ++it; continue; } + it = g_lockedRanges.erase(it); + if (rangeStart < start) g_lockedRanges[rangeStart] = start - rangeStart; + if (rangeEnd > end) g_lockedRanges[end] = rangeEnd - end; + } +} + } extern "C" { @@ -225,12 +257,17 @@ int32_t APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void** addr_in_out, si } int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot) { - return DoMprotect(addr, len, prot); + constexpr int DefinedProtection = 0x3F7; + if (len == 0) return 0; + return DoMprotect(addr, len, prot & DefinedProtection); } int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { const int result = DoMunmap(reinterpret_cast(vaddr), len); - if (result == 0) _releaseFlexible(static_cast(vaddr), len); + if (result == 0) { + _releaseFlexible(static_cast(vaddr), len); + ForgetLockedRange(static_cast(vaddr), static_cast(vaddr) + len); + } return result; } @@ -346,6 +383,7 @@ int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { } int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int prot) { + if (len == 0) return 0; return DoMtypeprotect(addr, len, type, prot); } @@ -524,7 +562,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const SIZE_T bytes = end - start; std::lock_guard lock(g_workingSetLock); - if (VirtualLock(address, bytes)) return 0; + if (VirtualLock(address, bytes)) { + RecordLockedRange(start, end); + return 0; + } if (GetLastError() != ERROR_WORKING_SET_QUOTA) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualLock failed"); SIZE_T minimum = 0; SIZE_T maximum = 0; @@ -532,7 +573,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &flags)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "GetProcessWorkingSetSizeEx failed"); if (bytes > std::numeric_limits::max() - maximum) return SCE_KERNEL_ERROR_EAGAIN; if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), minimum + bytes, maximum + bytes, flags)) return SCE_KERNEL_ERROR_EAGAIN; - if (VirtualLock(address, bytes)) return 0; + if (VirtualLock(address, bytes)) { + RecordLockedRange(start, end); + return 0; + } if (GetLastError() != ERROR_WORKING_SET_QUOTA) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualLock failed"); return SCE_KERNEL_ERROR_EAGAIN; } @@ -581,19 +625,26 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { } cursor = next; } + ForgetLockedRange(start, end); return 0; } #else int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const std::size_t bytes = end - start; - if (::mlock(address, bytes) == 0) return 0; + if (::mlock(address, bytes) == 0) { + RecordLockedRange(start, end); + return 0; + } int error = errno; rlimit limit{}; if ((error == ENOMEM || error == EPERM || error == EAGAIN) && getrlimit(RLIMIT_MEMLOCK, &limit) == 0 && limit.rlim_cur < limit.rlim_max) { limit.rlim_cur = limit.rlim_max; if (setrlimit(RLIMIT_MEMLOCK, &limit) == 0) { - if (::mlock(address, bytes) == 0) return 0; + if (::mlock(address, bytes) == 0) { + RecordLockedRange(start, end); + return 0; + } error = errno; } } @@ -607,7 +658,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GuestRangeMapped(start, end)) return SCE_KERNEL_ERROR_ENOMEM; - if (::munlock(reinterpret_cast(start), end - start) == 0) return 0; + if (::munlock(reinterpret_cast(start), end - start) == 0) { + ForgetLockedRange(start, end); + return 0; + } const int error = errno; if (error == ENOMEM) return SCE_KERNEL_ERROR_ENOMEM; throw std::system_error(error, std::generic_category(), "munlock failed"); @@ -634,4 +688,35 @@ int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length) return UnlockHostPages(start, end); } +int APS5_VABI sceKernelMunlockall_nid_postfix() { +#ifndef _WIN32 + ::munlockall(); +#endif + std::map ranges; + { + std::lock_guard lock(g_lockedRangesLock); + ranges.swap(g_lockedRanges); + } +#ifdef _WIN32 + for (const auto& [start, len] : ranges) { + const auto end = start + len; + for (auto cursor = start; cursor < end;) { + MEMORY_BASIC_INFORMATION region{}; + if (VirtualQuery(reinterpret_cast(cursor), ®ion, sizeof(region)) != sizeof(region)) break; + const auto next = std::min(end, reinterpret_cast(region.BaseAddress) + region.RegionSize); + if (region.State == MEM_COMMIT && (region.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0 && !VirtualUnlock(reinterpret_cast(cursor), next - cursor)) { + if (GetLastError() != ERROR_NOT_LOCKED) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualUnlock failed"); + for (auto page = cursor; page < next; page += HostPageSize()) { + if (!VirtualUnlock(reinterpret_cast(page), std::min(HostPageSize(), next - page)) && GetLastError() != ERROR_NOT_LOCKED) { + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualUnlock failed"); + } + } + } + cursor = next; + } + } +#endif + return 0; +} + } diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index c8049b48d9..250d252b47 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include #include @@ -19,6 +20,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI sceKernelMlock_nid_postfix(void* address, std::uint64_t length); extern "C" int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length); +extern "C" int APS5_VABI sceKernelMunlockall_nid_postfix(); extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size); namespace { @@ -133,6 +135,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti (mappingFlags & ~(GuestShared | GuestPrivate | GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce)) != 0) return failed(GuestNotSupported); if (descriptor < 0) return failed(GuestBadDescriptor); + File::RefuseArchivedFile(descriptor, "mmap"); if (offset < 0 || (static_cast(offset) & (PS5_PAGE_SIZE - 1)) != 0 || rounded > static_cast(std::numeric_limits::max()) || static_cast(offset) > static_cast(std::numeric_limits::max()) - rounded) @@ -240,6 +243,13 @@ int APS5_VABI mlockall_nid_postfix(int flags) { throw std::runtime_error("mlockall: MCL_CURRENT is not implemented: the process also holds host stacks, heaps, modules and driver mappings"); } +int APS5_VABI munlockall_nid_postfix() { + const int result = sceKernelMunlockall_nid_postfix(); + if (result == 0) return 0; + SetError(result & 0xffff); + return -1; +} + int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { const auto failed = [](int error) { SetError(error); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 641deca1d4..249f2959f0 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -50,7 +50,7 @@ void KernelEqueuePrivate::Close() { void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { for (auto& ev : m_events) { - if (ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { + if (ev.enabled && ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { if (ev.event.filter == EVFILT_TIMER) { const uint64_t count = ev.intervalNs == 0 ? (ev.triggered ? 0 : 1) @@ -66,7 +66,7 @@ void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const { uint64_t nearest = std::numeric_limits::max(); for (const auto& ev : m_events) { - if (!ev.triggered && ev.deadlineNs != 0) { + if (ev.enabled && !ev.triggered && ev.deadlineNs != 0) { nearest = std::min(nearest, ev.deadlineNs); } } @@ -82,7 +82,7 @@ bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) con bool KernelEqueuePrivate::PollEvents() { bool polled = false; for (auto& ev : m_events) { - if (ev.filter.pollFunc != nullptr) { + if (ev.enabled && ev.filter.pollFunc != nullptr) { ev.triggered = ev.filter.pollFunc(&ev); polled = true; } @@ -99,7 +99,7 @@ void KernelEqueuePrivate::WakePollers() { void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos) { std::vector interests; for (const auto& e : m_events) { - if (e.filter.pollFunc != nullptr) { + if (e.enabled && e.filter.pollFunc != nullptr) { interests.push_back({static_cast(e.event.ident), e.event.filter == EVFILT_WRITE}); } } @@ -121,6 +121,10 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { int ret = 0; for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { auto& e = *it; + if (!e.enabled) { + ++it; + continue; + } bool erase = false; while (e.triggered && ret < num) { ev[ret++] = e.event; @@ -128,6 +132,13 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { erase = true; break; } + if ((e.event.flags & EV_DISPATCH) != 0) { + e.enabled = false; + e.triggered = false; + e.event.fflags = 0; + e.event.data = 0; + break; + } if (e.filter.resetFunc != nullptr) { e.filter.resetFunc(&e); } else if ((e.event.flags & EV_CLEAR) != 0) { @@ -152,7 +163,7 @@ int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { std::list requeued; for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { auto& e = *it; - if (!e.triggered) { + if (!e.triggered || !e.enabled) { ++it; continue; } @@ -165,7 +176,12 @@ int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { it = m_events.erase(it); continue; } - if (e.filter.resetFunc != nullptr) { + if ((e.event.flags & EV_DISPATCH) != 0) { + e.enabled = false; + e.triggered = false; + e.event.fflags = 0; + e.event.data = 0; + } else if (e.filter.resetFunc != nullptr) { e.filter.resetFunc(&e); } else if ((e.event.flags & EV_CLEAR) != 0) { e.triggered = false; @@ -281,10 +297,12 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri if (it == m_events.end()) { return SCE_KERNEL_ERROR_ENOENT; } - if (it->filter.triggerFunc != nullptr) { - it->filter.triggerFunc(&*it, triggerData); - } else { - it->triggered = true; + if (it->enabled) { + if (it->filter.triggerFunc != nullptr) { + it->filter.triggerFunc(&*it, triggerData); + } else { + it->triggered = true; + } } m_cond.NotifyOne(); WakePollers(); @@ -637,6 +655,9 @@ KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { return false; } + if (!eof && data < e->lowWater && !GuestSockets::Listening(static_cast(e->event.ident))) { + 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; diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 8bc8d0671d..39ad8bf12d 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -58,8 +58,10 @@ struct KernelFilter { struct KernelEqueueEvent { bool triggered = false; + bool enabled = true; uint64_t deadlineNs = 0; uint64_t intervalNs = 0; + std::int64_t lowWater = 0; KernelEvent event; KernelFilter filter; std::deque pendingEvents; diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 1696650661..05cca27e4c 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -98,9 +98,6 @@ std::optional TimerPeriodNanos(std::int64_t data, std::uint32_t f 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: @@ -127,6 +124,22 @@ int DescriptorFilterError(int kqueueDescriptor, std::uintptr_t ident) { return descriptor == kqueueDescriptor ? GUEST_EINVAL : 0; } +std::int64_t DescriptorLowWater(const KernelEvent& change) { + if ((change.fflags & NOTE_LOWAT) == 0 || !GuestSockets::IsOpen(static_cast(change.ident))) { + return 0; + } + if (change.data < 1) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT below 1 on a socket"); + } + if (change.filter == EVFILT_WRITE && change.data > 1) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on EVFILT_WRITE"); + } + if (change.data > 1 && GuestSockets::Type(static_cast(change.ident)) != 1) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on a socket that is not a stream"); + } + return change.data; +} + int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { const int descriptor = static_cast(change.ident); if (IsKqueueDescriptor(descriptor)) { @@ -135,16 +148,19 @@ int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::u } 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"); + const bool socket = GuestSockets::IsOpen(descriptor); + std::int64_t data = 0; + bool eof = false; + const bool pipe = !socket && change.filter == EVFILT_READ && GuestSockets::PipeReadState(descriptor, &data, &eof); + if (!socket && !pipe) { + NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a non-socket descriptor"); } + const auto lowWater = DescriptorLowWater(change); if ((flags & EV_CLEAR) != 0) { - NotImplemented_nid_no_patch("kevent: EV_CLEAR on a socket"); + NotImplemented_nid_no_patch(pipe ? "kevent: EV_CLEAR on a pipe" : "kevent: EV_CLEAR on a socket"); } auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); + event.lowWater = lowWater; change.fflags = 0; change.data = 0; return GuestErrno(queue->AddEvent(event)); @@ -215,14 +231,26 @@ 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"); - } + const bool descriptor = change.filter == EVFILT_READ || change.filter == EVFILT_WRITE; + const auto lowWater = descriptor ? DescriptorLowWater(change) : 0; if (change.filter == EVFILT_VNODE) { RequireVnodeNotes(change.fflags); } - return GuestErrno(queue->EditEvent(change.ident, change.filter, [&change](KernelEqueueEvent& event) { + return GuestErrno(queue->EditEvent(change.ident, change.filter, [&change, descriptor, lowWater](KernelEqueueEvent& event) { event.event.udata = change.udata; + if (descriptor) { + event.lowWater = lowWater; + } + if ((change.flags & EV_ENABLE) != 0) { + event.enabled = true; + } + if ((change.flags & EV_DISABLE) != 0) { + event.enabled = false; + } + if ((change.flags & EV_FORCEONESHOT) != 0) { + event.event.flags = static_cast(event.event.flags | EV_ONESHOT); + event.triggered = true; + } if (change.filter == EVFILT_USER) { TouchUserKnote(event, change); } else if (change.filter == EVFILT_VNODE) { @@ -241,15 +269,6 @@ int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& ch 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; @@ -260,19 +279,23 @@ int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& ch return GUEST_ENOENT; } const auto flags = static_cast(change.flags & ~KNOTE_CLEARED_FLAGS); + int error; 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) { + error = AddTimerKnote(queue, change, flags); + } else if (change.filter == EVFILT_USER) { + error = AddUserKnote(queue, change, flags); + } else 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); + error = AddVnodeEvent(queue, static_cast(change.ident), notes, flags, change.udata); + } else { + error = AddDescriptorKnote(queue, change, flags); } - return AddDescriptorKnote(queue, change, flags); + if (error == 0 && (change.flags & EV_DISABLE) != 0) { + queue->EditEvent(change.ident, change.filter, [](KernelEqueueEvent& event) { event.enabled = false; }); + } + return error; } if ((change.flags & EV_DELETE) != 0) { return GuestErrno(queue->DeleteEvent(change.ident, change.filter)); @@ -338,7 +361,7 @@ int APS5_VABI kevent_nid_postfix(int descriptor, const KernelEvent* changelist, return PosixFailure(GUEST_EBADF); } if (nevents < 0) { - NotImplemented_nid_no_patch("kevent: negative nevents"); + return PosixFailure(GUEST_EINVAL); } if (nchanges > 0 && changelist == nullptr) { return PosixFailure(GUEST_EFAULT); diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 7d4ebe2ea3..1e52870311 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -66,6 +66,7 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui (void)param; if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; + if ((attr & ~0x33u) != 0) return SCE_KERNEL_ERROR_EINVAL; auto* flag = new (std::nothrow) KernelEventFlagPrivate(); if (!flag) return SCE_KERNEL_ERROR_ENOMEM; flag->_name = name; diff --git a/core/libs/prx/libkernel/File/include/ArchivedFile.hpp b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp new file mode 100644 index 0000000000..73f84c1cca --- /dev/null +++ b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_ARCHIVEDFILE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_ARCHIVEDFILE_HPP + +#include +#include +#include +#include + +#include "SceTypes.hpp" + +namespace File { + +std::optional OpenArchivedFile(const std::filesystem::path& native, int flags); +void RefuseMergedDirectory(const std::filesystem::path& native); +bool IsArchivedFile(int fd); +void RefuseArchivedFile(int fd, const char* operation); +std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset); +std::optional ReadArchivedIovecs(int fd, const void* iov, int iovcnt, const std::int64_t* offset); +std::optional SeekArchivedFile(int fd, std::int64_t offset, int whence); +std::optional ArchivedFileRemaining(int fd); +bool FillArchivedStat(const std::filesystem::path& native, FileStat* sb); +bool FillArchivedStatFromDescriptor(int fd, FileStat* sb); +void ShareArchivedFile(int from, int to); +void ForgetArchivedFile(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index cb327dff54..d7345faec2 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -14,6 +14,7 @@ int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); +bool Appends(int fd); } diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 4a14063e0e..d759d1e3f9 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -30,6 +30,9 @@ namespace File { int HostStandardError(); [[nodiscard]] std::unique_lock LockPositionedTransfer(int descriptor); void SharePositionedTransfer(int from, int to); +#ifdef _WIN32 +std::int64_t WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes); +#endif } #endif diff --git a/core/libs/prx/libkernel/File/include/TrustedRoots.hpp b/core/libs/prx/libkernel/File/include/TrustedRoots.hpp new file mode 100644 index 0000000000..950d25dcff --- /dev/null +++ b/core/libs/prx/libkernel/File/include/TrustedRoots.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_TRUSTEDROOTS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_TRUSTEDROOTS_HPP + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +extern "C" std::string ExportHostRoots_nid_no_patch(HCERTSTORE roots, HCERTSTORE disallowed); +extern "C" std::string HostTrustedTlsRoots_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libkernel/File/src/ArchivedFile.cpp b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp new file mode 100644 index 0000000000..df3a8548b4 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp @@ -0,0 +1,121 @@ +#include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +namespace { + +constexpr std::uint16_t GuestRegularMode = 0100444; +constexpr std::uint16_t GuestDirectoryMode = 0040555; + +int OpenPlaceholder() { +#ifdef _WIN32 + return ::_open("NUL", _O_RDONLY | _O_BINARY); +#else + return ::open("/dev/null", O_RDONLY | O_CLOEXEC); +#endif +} + +void FillNode(const AppArchiveNode& node, FileStat* sb) { + File::FillFileStat(AppArchivePath_nid_no_patch(), sb); + sb->st_mode = node.Directory ? GuestDirectoryMode : GuestRegularMode; + sb->st_ino = node.Handle + 1; + sb->st_nlink = node.Directory ? 2 : 1; + sb->st_size = static_cast(node.Size); + sb->st_blocks = static_cast((node.Size + 511) / 512); +} + +} + +namespace File { + +std::optional OpenArchivedFile(const std::filesystem::path& native, int flags) { + if (!AppArchiveOnly_nid_no_patch(native)) return std::nullopt; + AppArchiveNode node{}; + if (!AppArchiveLookup_nid_no_patch(native, &node)) throw std::logic_error("OpenArchivedFile: " + native.string() + " left app0.zar while opening"); + if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) return -EEXIST; + if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & SCE_KERNEL_O_TRUNC) != 0) return -EROFS; + if (node.Directory) NotImplemented_nid_no_patch(("opening the app0.zar directory " + native.string()).c_str()); + if ((flags & SCE_KERNEL_O_DIRECTORY) != 0) return -ENOTDIR; + const int fd = OpenPlaceholder(); + if (fd < 0) return -errno; + AppArchiveAttach_nid_no_patch(fd, node); + return fd; +} + +void RefuseMergedDirectory(const std::filesystem::path& native) { + if (!AppArchiveHasDirectory_nid_no_patch(native)) return; + std::error_code error; + if (std::filesystem::is_directory(native, error)) NotImplemented_nid_no_patch(("opening " + native.string() + " merged with app0.zar").c_str()); +} + +bool IsArchivedFile(int fd) { + return AppArchiveDescriptor_nid_no_patch(fd, nullptr, nullptr); +} + +void RefuseArchivedFile(int fd, const char* operation) { + if (IsArchivedFile(fd)) NotImplemented_nid_no_patch((std::string(operation) + " on a file read from app0.zar, fd=" + std::to_string(fd)).c_str()); +} + +std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset) { + const AppArchiveBuffer buffer{buf, nbytes}; + return ReadArchivedIovecs(fd, &buffer, 1, offset); +} + +std::optional ReadArchivedIovecs(int fd, const void* iov, int iovcnt, const std::int64_t* offset) { + if (!IsArchivedFile(fd)) return std::nullopt; + if (offset != nullptr && *offset < 0) return -EINVAL; + std::uint64_t read = 0; + if (!AppArchiveDescriptorRead_nid_no_patch(fd, static_cast(iov), iovcnt, offset, &read)) return std::nullopt; + return static_cast(read); +} + +std::optional SeekArchivedFile(int fd, std::int64_t offset, int whence) { + std::int64_t position = 0; + if (!AppArchiveDescriptorSeek_nid_no_patch(fd, offset, whence, &position)) return std::nullopt; + return position < 0 ? -EINVAL : position; +} + +std::optional ArchivedFileRemaining(int fd) { + AppArchiveNode node{}; + std::uint64_t position = 0; + if (!AppArchiveDescriptor_nid_no_patch(fd, &node, &position)) return std::nullopt; + return position >= node.Size ? 0 : static_cast(node.Size - position); +} + +bool FillArchivedStat(const std::filesystem::path& native, FileStat* sb) { + if (!AppArchiveOnly_nid_no_patch(native)) return false; + AppArchiveNode node{}; + if (!AppArchiveLookup_nid_no_patch(native, &node)) throw std::logic_error("FillArchivedStat: " + native.string() + " left app0.zar while reading its status"); + FillNode(node, sb); + return true; +} + +bool FillArchivedStatFromDescriptor(int fd, FileStat* sb) { + AppArchiveNode node{}; + if (!AppArchiveDescriptor_nid_no_patch(fd, &node, nullptr)) return false; + FillNode(node, sb); + return true; +} + +void ShareArchivedFile(int from, int to) { + AppArchiveShare_nid_no_patch(from, to); +} + +void ForgetArchivedFile(int fd) { + AppArchiveDetach_nid_no_patch(fd); +} + +} diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 3e7f3c0644..174f0571b1 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -2,10 +2,12 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/FileLock.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -105,7 +107,7 @@ static_assert(sizeof(GuestLock) == 32); static_assert(offsetof(GuestLock, type) == 20); constexpr std::size_t OldGuestLockSize = 24; -enum class Kind { File, Directory, Pipe, Device, RandomDevice }; +enum class Kind { File, Directory, Pipe, Device, RandomDevice, ArchivedFile }; struct Entry { bool closeOnExec = false; @@ -127,6 +129,7 @@ const char* KindName(Kind kind) { case Kind::Pipe: return "pipe"; case Kind::Device: return "character device"; case Kind::RandomDevice: return "random device"; + case Kind::ArchivedFile: return "file read from app0.zar"; } return "descriptor"; } @@ -140,6 +143,7 @@ std::optional Classify(int fd, const char* function) { if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (File::IsArchivedFile(fd)) return Kind::ArchivedFile; switch (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd))) { case FILE_TYPE_DISK: return Kind::File; case FILE_TYPE_PIPE: return Kind::Pipe; @@ -259,6 +263,7 @@ std::optional Classify(int fd, const char* function) { throw std::runtime_error(std::string(function) + ": fstat failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); } if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (File::IsArchivedFile(fd)) return Kind::ArchivedFile; if (S_ISREG(status.st_mode)) return Kind::File; if (S_ISDIR(status.st_mode)) return Kind::Directory; if (S_ISFIFO(status.st_mode)) return Kind::Pipe; @@ -374,10 +379,8 @@ int RecordLock(int fd, int command, std::intptr_t argument) { struct flock host{}; if (lock.type == ReadLock) host.l_type = F_RDLCK; else if (lock.type == WriteLock) host.l_type = F_WRLCK; - else if (lock.type == Unlock) host.l_type = F_UNLCK; + else if (lock.type == Unlock) host.l_type = current == GetLock ? F_RDLCK : F_UNLCK; else return Fail(GuestEinval); - if (current == GetLock && lock.type == Unlock) - throw std::runtime_error("fcntl: F_GETLK with F_UNLCK is not implemented, fd=" + std::to_string(fd)); host.l_whence = lock.whence; host.l_start = static_cast(lock.start); host.l_len = static_cast(lock.length); @@ -409,7 +412,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { int ReadAheadControl(int fd, int command, std::intptr_t argument) { const auto kind = Classify(fd, "fcntl"); if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); - if (*kind != Kind::File) return Fail(GuestEnotty); + if (*kind != Kind::File && *kind != Kind::ArchivedFile) return Fail(GuestEnotty); std::lock_guard lock(g_mutex); auto& status = *Materialize(fd).status; if (command == DarwinReadAhead || argument >= 0) status |= ReadAheadFlag; @@ -521,8 +524,9 @@ int Ioctl(int fd, std::uint64_t request, void* argument) { } if (command == ReadableRequest) { if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); - if (*kind != Kind::File && *kind != Kind::Pipe) Unsupported("ioctl", "FIONREAD", *kind, fd); - const int available = Readable(fd, *kind, "ioctl"); + if (*kind != Kind::File && *kind != Kind::Pipe && *kind != Kind::ArchivedFile) Unsupported("ioctl", "FIONREAD", *kind, fd); + const auto archived = File::ArchivedFileRemaining(fd); + const int available = archived ? static_cast(std::min(*archived, std::numeric_limits::max())) : Readable(fd, *kind, "ioctl"); if (argument == nullptr) return Fail(GuestEfault); const GuestArena::HostWrite destination(argument, sizeof(available)); if (!destination.Open()) return Fail(GuestEfault); @@ -544,6 +548,7 @@ void RememberDescriptor(int fd, int openFlags) { } void ShareDescriptor(int from, int to) { + File::ShareArchivedFile(from, to); std::lock_guard lock(g_mutex); auto status = Materialize(from).status; g_entries[to] = Entry{false, std::move(status)}; @@ -554,8 +559,15 @@ void ShareDescriptor(int from, int to) { } void ForgetDescriptor(int fd) { + File::ForgetArchivedFile(fd); std::lock_guard lock(g_mutex); g_entries.erase(fd); } +bool Appends(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_entries.find(fd); + return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; +} + } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 9d097eec22..b110e980df 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libc/include/General.hpp" #include @@ -156,6 +157,7 @@ void FillFileStat(int nativeDescriptor, FileStat* sb) { } bool FillFileStatFromDescriptor(int fd, FileStat* sb) { + if (FillArchivedStatFromDescriptor(fd, sb)) return true; NativeStat st{}; if (DoFstat(fd, &st) != 0) return false; CopyNativeStat(st, sb); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 9760bbe879..23fd92fc59 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -12,6 +12,7 @@ #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/HostStreams.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" @@ -73,6 +74,10 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) { const auto positioned = File::LockPositionedTransfer(fd); const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); if (shared != -2) return static_cast(shared); + if (File::Appends(fd)) { + const auto appended = File::WriteAtEnd(fd, buf, n); + if (appended != -2) return static_cast(appended); + } const int result = ::_write(fd, buf, static_cast(n)); if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; return result; @@ -96,6 +101,17 @@ static int NativeUnlink(const std::filesystem::path& p) { errno = error; return -1; } +static std::filesystem::path NativeMetadataPath(const std::filesystem::path& path, std::error_code& error) { + const auto& input = path.native(); + if (input.starts_with(L"\\\\?\\") || input.starts_with(L"\\\\.\\")) return path; + auto absolute = std::filesystem::absolute(path, error); + if (error) return {}; + absolute = absolute.lexically_normal().make_preferred(); + const auto& native = absolute.native(); + if (native.size() < MAX_PATH || native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return absolute; + return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) + : std::filesystem::path(L"\\\\?\\" + native); +} static int MapFlags(int sceFlags) { int f = 0; const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; @@ -174,6 +190,12 @@ static int SceIoErrorFromErrno(int error) { } static int OpenNative(const std::filesystem::path& native, int flags, std::uint16_t mode) { + if (const auto archived = File::OpenArchivedFile(native, flags)) { + if (*archived < 0) return SceErrorFromErrno(-*archived); + File::RememberDescriptor(*archived, flags); + return *archived; + } + File::RefuseMergedDirectory(native); #ifdef _WIN32 if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; @@ -222,7 +244,7 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } std::filesystem::path native; const bool module = (flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY; - if (const int error = ResolvePathError_nid_no_patch(path, native, module)) return SceKernelError(error); + if (const int error = ResolvePathError_nid_no_patch(path, native, module, true)) return SceKernelError(error); return OpenNative(native, flags, mode); } @@ -236,6 +258,9 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std int APS5_VABI sceKernelClose(int d) { if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); +#ifdef _WIN32 + const auto positioned = File::LockPositionedTransfer(d); +#endif EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); @@ -257,11 +282,14 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } if (buf == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": buf is null"); + if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + char empty = 0; + return sceKernelRead(d, &empty, 0); } const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) errno = EFAULT; else if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + else if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, nullptr)) return *archived; auto n = destination.Open() ? NativeRead(d, buf, nbytes) : -1; if (n < 0) return SceIoErrorFromErrno(errno); return static_cast(n); @@ -273,7 +301,9 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } if (buf == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": buf is null"); + if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + const char empty = 0; + return sceKernelWrite(d, &empty, 0); } auto n = NativeWrite(d, buf, nbytes); if (n < 0) return SceIoErrorFromErrno(errno); @@ -285,6 +315,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } + if (const auto archived = File::SeekArchivedFile(d, offset, whence)) return *archived < 0 ? SceIoErrorFromErrno(-*archived) : *archived; #ifdef _WIN32 if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { if (*directory < 0) return SceIoErrorFromErrno(errno); @@ -296,17 +327,28 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { return result; } +constexpr int GuestEfault = 14; + int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": path is null"); - } + if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return SceKernelError(error); + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); std::error_code error; constexpr int GuestEnotdir = 20; - constexpr int GuestEfault = 14; - const auto status = std::filesystem::status(native, error); + FileStat archived{}; + if (File::FillArchivedStat(native, &archived)) { + if (std::string_view(path).back() == '/' && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); + *sb = archived; + return 0; + } + auto metadataPath = native; +#ifdef _WIN32 + metadataPath = NativeMetadataPath(native, error); + if (error) return SceErrorFromErrno(FilesystemError(error)); +#endif + const auto status = std::filesystem::status(metadataPath, error); if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (!std::filesystem::exists(status)) { return SceErrorFromErrno(2); @@ -314,14 +356,12 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { const std::string_view guestPath(path); if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); - File::FillFileStat(native, sb); + File::FillFileStat(metadataPath, sb); return 0; } int APS5_VABI sceKernelUnlink(const char* path) { - if (path == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": path is null"); - } + if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); diff --git a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp new file mode 100644 index 0000000000..d41befaec6 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include "prx/libkernel/File/include/TrustedRoots.hpp" +#else +#include +#endif + +extern "C" const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); + +namespace { +constexpr char CertificateBegin[] = "-----BEGIN CERTIFICATE-----"; + +std::optional ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (file.bad()) throw std::runtime_error("Reading " + path.string() + " failed"); + return text; +} + +#ifdef _WIN32 +std::string HostCertificates() { + return HostTrustedTlsRoots_nid_no_patch(); +} + +unsigned long ProcessId() { return GetCurrentProcessId(); } +#else +std::string HostCertificates() { + std::array candidates{std::getenv("SSL_CERT_FILE"), "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem", "/etc/ssl/cert.pem"}; + for (const char* candidate : candidates) { + std::error_code error; + if (candidate == nullptr || *candidate == '\0' || !std::filesystem::is_regular_file(candidate, error)) continue; + if (auto text = ReadFile(candidate)) return *text; + } + return {}; +} + +unsigned long ProcessId() { return static_cast(getpid()); } +#endif + +std::filesystem::path WriteCertificateList(const std::filesystem::path& root) { + const auto bundle = HostCertificates(); + if (bundle.find(CertificateBegin) == std::string::npos) return {}; + const auto directory = root / "cert_cache"; + std::filesystem::create_directories(directory); + const auto target = directory / "CA_LIST.cer"; + if (ReadFile(target) == bundle) return target; + auto temporary = target; + temporary += "." + std::to_string(ProcessId()) + ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + file.write(bundle.data(), static_cast(bundle.size())); + if (!file.flush()) throw std::runtime_error("Writing " + temporary.string() + " failed"); + } + using std::filesystem::perms; + std::filesystem::permissions(temporary, perms::owner_read | perms::group_read | perms::others_read); + std::error_code error; + if (std::filesystem::exists(target, error)) std::filesystem::permissions(target, perms::owner_write, std::filesystem::perm_options::add); + std::filesystem::rename(temporary, target); + return target; +} + +std::filesystem::path CertificateList(const std::filesystem::path& root) { + static const std::filesystem::path list = WriteCertificateList(root); + return list; +} + +const bool g_certificateListProvided = [] { + AddPathProvider_nid_no_patch((std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert/CA_LIST.cer").c_str(), &CertificateList); + return true; +}(); +} diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5fd51f5386..684d0b121a 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -17,6 +17,8 @@ #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/HostStreams.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -41,6 +43,7 @@ struct KernelIovec { std::size_t length; }; static_assert(sizeof(KernelIovec) == sizeof(GuestSockets::Iovec)); +static_assert(sizeof(KernelIovec) == sizeof(AppArchiveBuffer) && offsetof(KernelIovec, length) == offsetof(AppArchiveBuffer, Length)); static constexpr int KERNEL_IOV_MAX = 1024; @@ -112,8 +115,9 @@ static int NativeFtruncate(int descriptor, std::int64_t length) { const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); if (result != -2) return result; const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); - if (handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; - return static_cast(::_chsize_s(descriptor, length)); + if (handle == INVALID_HANDLE_VALUE) return EBADF; + if (::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; + return static_cast(WithoutParameterHandler([descriptor, length] { return ::_chsize_s(descriptor, length); })); } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); @@ -198,7 +202,7 @@ void File::SharePositionedTransfer(int from, int to) { 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) { +static std::int64_t NativePositionedTransfer(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"); } @@ -211,7 +215,6 @@ 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 = !File::DirectoryDescriptorPath(descriptor) ? ESPIPE : write ? EBADF : EISDIR; @@ -234,17 +237,49 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ } return done; } -static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes, bool write) { +std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { + const auto positioned = File::LockPositionedTransfer(descriptor); + return NativePositionedTransfer(descriptor, buf, nbytes, offset, write); +} +std::int64_t File::WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes) { + if (nbytes > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("WriteAtEnd: nbytes exceeds platform limit"); + } + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + if (::GetFileType(handle) != FILE_TYPE_DISK) return -2; + OVERLAPPED end{}; + end.Offset = 0xFFFFFFFF; + end.OffsetHigh = 0xFFFFFFFF; + DWORD done = 0; + if (!::WriteFile(handle, buf, static_cast(nbytes), &done, &end)) { + const DWORD error = ::GetLastError(); + errno = error == ERROR_ACCESS_DENIED ? EBADF : error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; + return -1; + } + return done; +} +static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t nbytes, bool write) { 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; + if (File::Appends(descriptor)) { + const auto appended = File::WriteAtEnd(descriptor, buf, nbytes); + if (appended != -2) return appended; + } } return write ? ::_write(descriptor, buf, static_cast(nbytes)) : ::_read(descriptor, buf, static_cast(nbytes)); } +static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes, bool write) { + const auto positioned = File::LockPositionedTransfer(descriptor); + return NativeTransferBuffer(descriptor, buf, nbytes, write); +} static bool NativeIsDisk(int descriptor) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_DISK; @@ -381,6 +416,10 @@ static int PosixFailure(int error) { return -1; } +static bool ExceedsTransferLimit(std::uint64_t bytes) { + return bytes > static_cast(std::numeric_limits::max()); +} + static int PosixResult(int result) { return result < 0 ? PosixFailure(result & 0xffff) : result; } @@ -565,6 +604,14 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { return 0; } +int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int flags) { + (void)iov; + (void)niov; + (void)flags; + NotImplemented_nid_no_patch("nmount: mounting file systems"); + return -1; +} + int APS5_VABI close_nid_postfix(int d) { if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); int socketError = 0; @@ -574,6 +621,9 @@ int APS5_VABI close_nid_postfix(int d) { return -1; } if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d); +#ifdef _WIN32 + const auto positioned = File::LockPositionedTransfer(d); +#endif EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); @@ -644,6 +694,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (GuestSockets::DuplicateTo(from, to) < 0) return -1; if (from == to) return to; File::ForgetRandomDevice(to); + File::ForgetArchivedFile(to); #ifdef _WIN32 File::ForgetDirectoryDescriptor(to); File::ForgetFileLock(to); @@ -690,6 +741,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { int APS5_VABI flock_nid_postfix(int d, int operation) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); + File::RefuseArchivedFile(d, "flock"); 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) { @@ -709,6 +761,7 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EINVAL); if (d < 0) return PosixFailure(GUEST_EBADF); + File::RefuseArchivedFile(d, "fstatfs"); KernelStatfs result{}; result.f_version = KERNEL_STATFS_VERSION; if (const int error = NativeFstatfs(d, &result)) return PosixFailure(error); @@ -721,7 +774,9 @@ int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (sb == nullptr) { - APS5_INVALID_ARG_EX; + FileStat ignored{}; + if (fstat_nid_disambig1_nid_postfix(d, &ignored) != 0) return -1; + return PosixFailure(GUEST_EFAULT); } if (GuestSockets::IsOpen(d)) { if (const int error = GuestSockets::Stat(d, sb)) return PosixFailure(error); @@ -735,6 +790,7 @@ int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); } + File::RefuseArchivedFile(d, "ftruncate"); #ifdef _WIN32 int error = NativeFtruncate(d, length); if (error != 0) { @@ -799,18 +855,21 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return PosixFailure(GUEST_EFAULT); if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; char empty = 0; const auto result = NativePread(d, buf == nullptr ? &empty : buf, nbytes, offset); return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; @@ -820,6 +879,7 @@ 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 (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); char emptyBuffer = 0; @@ -827,6 +887,7 @@ int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { } std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); char emptyBuffer = 0; @@ -834,9 +895,7 @@ std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size } int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { - if (size < 0) { - APS5_INVALID_ARG_EX; - } + if (size < 0) return PosixFailure(GUEST_EINVAL); 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; @@ -844,6 +903,7 @@ int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { } std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Write(descriptor, buffer, count); if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); const char emptyBuffer = 0; @@ -875,13 +935,18 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return SceKernelError(error); + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); + if (AppArchiveOnly_nid_no_patch(native)) return 0; 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 (sb == nullptr) { + FileStat ignored{}; + if (const int error = sceKernelFstat(d, &ignored)) return error; + return SceErrorFromErrno(GUEST_EFAULT); + } if (GuestSockets::IsOpen(d)) { const int error = GuestSockets::Stat(d, sb); return error == 0 ? 0 : SceErrorFromErrno(error); @@ -892,6 +957,7 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { int APS5_VABI sceKernelFsync(int fd) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "fsync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else @@ -902,6 +968,7 @@ int APS5_VABI sceKernelFsync(int fd) { int APS5_VABI sceKernelFdatasync(int fd) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "fdatasync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else @@ -911,7 +978,7 @@ int APS5_VABI sceKernelFdatasync(int fd) { } int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { - if (status == nullptr) throw std::invalid_argument("sceKernelWriteThrottlingStatus: status is null"); + if (status == nullptr) return SceErrorFromErrno(GUEST_EFAULT); status[0] = std::numeric_limits::max(); status[1] = 0; status[2] = 0; @@ -923,6 +990,7 @@ int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "getdirentries"); if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); if (basep != nullptr) *basep = 0; @@ -937,6 +1005,7 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "getdirentries"); constexpr std::size_t GuestHeaderBytes = 8; constexpr std::size_t GuestMaxName = 255; if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); @@ -992,7 +1061,7 @@ int APS5_VABI getdents_nid_postfix(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; - if (path == nullptr) throw std::invalid_argument("sceKernelMkdir: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (!*path) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -1038,6 +1107,7 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; char empty = 0; const auto result = NativePread(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; @@ -1073,19 +1143,33 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } if (!write && File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + if (!write) { + if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, offset)) return *archived; + } + const auto positioned = File::LockPositionedTransfer(d); if (total == 0) { char none = 0; - const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, &none, 0, *offset, write) : NativeTransfer(d, &none, 0, write); + const auto result = offset != nullptr ? NativePositionedTransfer(d, &none, 0, *offset, write) : NativeTransferBuffer(d, &none, 0, write); return result < 0 ? SceErrorFromErrno(errno) : 0; } if (!write && offset == nullptr && File::DirectoryDescriptorPath(d)) return SceErrorFromErrno(GUEST_EISDIR); + if (write && offset == nullptr && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { + std::vector gathered; + gathered.reserve(total); + for (int i = 0; i < iovcnt; ++i) { + const auto* base = static_cast(iov[i].base); + gathered.insert(gathered.end(), base, base + iov[i].length); + } + const auto result = NativeTransferBuffer(d, gathered.data(), gathered.size(), true); + return result < 0 ? SceErrorFromErrno(errno) : result; + } const bool whole = write || offset != nullptr || NativeIsDisk(d); std::int64_t done = 0; for (int i = 0; i < iovcnt; ++i) { auto* base = static_cast(iov[i].base); for (std::size_t position = 0; position < iov[i].length;) { const auto chunk = std::min(iov[i].length - position, std::numeric_limits::max()); - const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, base + position, chunk, *offset + done, write) : NativeTransfer(d, base + position, chunk, write); + const auto result = offset != nullptr ? NativePositionedTransfer(d, base + position, chunk, *offset + done, write) : NativeTransferBuffer(d, base + position, chunk, write); if (result < 0) return done != 0 ? done : SceErrorFromErrno(errno); done += result; position += static_cast(result); @@ -1120,6 +1204,7 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; const auto result = NativePread(d, buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1141,6 +1226,7 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, nullptr)) return *archived; const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1164,6 +1250,7 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int std::deque destinations; if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, &offset)) return *archived; const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1191,7 +1278,7 @@ int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, } int APS5_VABI sceKernelRename(const char* from, const char* to) { - if (from == nullptr || to == nullptr) throw std::invalid_argument("sceKernelRename: path is null"); + if (!from || !to) return SceErrorFromErrno(GUEST_EFAULT); if (*from == '\0' || *to == '\0') return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path source; if (const int error = ResolvePathError_nid_no_patch(from, source)) return SceKernelError(error); @@ -1213,7 +1300,7 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { } int APS5_VABI sceKernelRmdir(const char* path) { - if (path == nullptr) throw std::invalid_argument("sceKernelRmdir: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -1250,6 +1337,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 (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(d, "fchmod"); if (NativeFchmod(d, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } @@ -1259,7 +1347,7 @@ int APS5_VABI fchmod_nid_postfix(int d, int mode) { } int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { - if (path == nullptr) throw std::invalid_argument("sceKernelTruncate: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); std::filesystem::path native; @@ -1279,7 +1367,7 @@ int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { - if (path == nullptr) throw std::invalid_argument("sceKernelUtimes: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (times != nullptr) { for (int i = 0; i < 2; ++i) { @@ -1312,6 +1400,7 @@ int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) int APS5_VABI sceKernelFutimes(int d, const KernelTimeval* times) { if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(d, "futimes"); 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/TrustedRoots.cpp b/core/libs/prx/libkernel/File/src/TrustedRoots.cpp new file mode 100644 index 0000000000..e5f1f4f0c4 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/TrustedRoots.cpp @@ -0,0 +1,78 @@ +#include "prx/libkernel/File/include/TrustedRoots.hpp" + +#include +#include +#include +#include +#include + +namespace { + +void RequireCrypto(bool success, const char* operation) { + if (!success) throw std::system_error(static_cast(GetLastError()), std::system_category(), operation); +} + +bool AllowsServerAuthentication(PCCERT_CONTEXT certificate) { + DWORD size = 0; + RequireCrypto(CertGetEnhancedKeyUsage(certificate, 0, nullptr, &size), "Reading root certificate usage size"); + std::vector storage(size); + auto* usage = reinterpret_cast(storage.data()); + SetLastError(ERROR_SUCCESS); + RequireCrypto(CertGetEnhancedKeyUsage(certificate, 0, usage, &size), "Reading root certificate usage"); + if (usage->cUsageIdentifier == 0) return GetLastError() == static_cast(CRYPT_E_NOT_FOUND); + for (DWORD index = 0; index < usage->cUsageIdentifier; ++index) { + const auto* identifier = usage->rgpszUsageIdentifier[index]; + if (std::strcmp(identifier, szOID_PKIX_KP_SERVER_AUTH) == 0 || std::strcmp(identifier, szOID_ANY_ENHANCED_KEY_USAGE) == 0) + return true; + } + return false; +} + +std::string Pem(PCCERT_CONTEXT certificate) { + DWORD size = 0; + RequireCrypto(CryptBinaryToStringA(certificate->pbCertEncoded, certificate->cbCertEncoded, + CRYPT_STRING_BASE64HEADER, nullptr, &size), "Measuring a PEM root certificate"); + std::string pem(size, '\0'); + RequireCrypto(CryptBinaryToStringA(certificate->pbCertEncoded, certificate->cbCertEncoded, + CRYPT_STRING_BASE64HEADER, pem.data(), &size), "Encoding a PEM root certificate"); + pem.resize(size); + return pem; +} + +} + +extern "C" std::string ExportHostRoots_nid_no_patch(HCERTSTORE roots, HCERTSTORE disallowed) { + if (!roots || !disallowed) throw std::invalid_argument("ExportHostRoots: null certificate store"); + std::string bundle; + using Certificate = std::unique_ptr; + Certificate certificate(nullptr, &CertFreeCertificateContext); + for (;;) { + auto* next = CertEnumCertificatesInStore(roots, certificate.release()); + if (!next) { + const auto error = GetLastError(); + if (error != static_cast(CRYPT_E_NOT_FOUND)) + throw std::system_error(static_cast(error), std::system_category(), "Enumerating trusted roots"); + break; + } + certificate.reset(next); + Certificate rejected(CertFindCertificateInStore(disallowed, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_EXISTING, certificate.get(), nullptr), &CertFreeCertificateContext); + if (rejected) continue; + const auto error = GetLastError(); + if (error != static_cast(CRYPT_E_NOT_FOUND)) + throw std::system_error(static_cast(error), std::system_category(), "Checking distrusted roots"); + if (AllowsServerAuthentication(certificate.get())) bundle += Pem(certificate.get()); + } + return bundle; +} + +extern "C" std::string HostTrustedTlsRoots_nid_no_patch() { + const auto closeStore = [](void* store) { CertCloseStore(store, 0); }; + using Store = std::unique_ptr; + const DWORD flags = CERT_SYSTEM_STORE_CURRENT_USER | CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG; + Store roots(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, flags, L"ROOT"), closeStore); + RequireCrypto(roots != nullptr, "Opening the Windows trusted root store"); + Store disallowed(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, flags, L"Disallowed"), closeStore); + RequireCrypto(disallowed != nullptr, "Opening the Windows disallowed certificate store"); + return ExportHostRoots_nid_no_patch(roots.get(), disallowed.get()); +} diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 4dac33c1dd..aa9a90cb4b 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -217,6 +217,8 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { if (auto* result = DefaultScopeSymbol(name)) return result; const auto nid = Nid::ComputeNid(name, ""); if (auto* result = DefaultScopeSymbol(nid.c_str())) return result; + if (auto* result = DefaultScopeSymbol((std::string(name) + "#guest").c_str())) return result; + if (auto* result = DefaultScopeSymbol((nid + "#guest").c_str())) return result; } #endif Error("dlsym: symbol not found in supported module scope"); @@ -226,26 +228,24 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { int APS5_VABI dlclose_nid_postfix(void* handle) { const auto key = reinterpret_cast(handle); std::shared_ptr module; -#ifdef _WIN32 bool lastReference = true; -#endif { std::lock_guard lock(modulesMutex); auto found = modules.find(key); if (found == modules.end()) { Error("dlclose: invalid module handle"); return -1; } module = found->second; -#ifdef _WIN32 for (const auto& [other, entry] : modules) { if (other != key && entry->native == module->native) lastReference = false; } -#endif } -#ifdef _WIN32 if (module->owned && module->native && lastReference) { GuestAllocations::Mutation mutation; +#ifdef _WIN32 mutation.UnregisterImage(module->native); - } +#else + mutation.UnregisterImage(reinterpret_cast(static_cast(module->native)->l_addr)); #endif + } { std::lock_guard lock(modulesMutex); auto found = modules.find(key); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index ea6ce0c0b1..1ea13b5de0 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -13,8 +13,13 @@ int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize); int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* addr, std::size_t size); int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched); +int APS5_VABI sched_get_priority_max_nid_postfix(int policy); +int APS5_VABI sched_get_priority_min_nid_postfix(int policy); } +static constexpr int GuestSchedFifo = 1; +static constexpr int GuestSchedRr = 3; + static bool Valid(const PthreadAttr* attr) { return attr && *attr; } @@ -120,13 +125,19 @@ int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int in } int APS5_VABI pthread_attr_setschedparam_nid_postfix(PthreadAttr* attr, const KernelSchedParam* param) { - if (!Valid(attr) || !param) return PosixThread::GUEST_EINVAL; + if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (!param) return PosixThread::GUEST_ENOTSUP; + const int policy = (*attr)->_schedpolicy; + if ((policy == GuestSchedFifo || policy == GuestSchedRr) && + (param->sched_priority < sched_get_priority_min_nid_postfix(policy) || param->sched_priority > sched_get_priority_max_nid_postfix(policy))) + return PosixThread::GUEST_ENOTSUP; (*attr)->_schedpriority = param->sched_priority; return 0; } int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int policy) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (policy < GuestSchedFifo || policy > GuestSchedRr) return PosixThread::GUEST_ENOTSUP; (*attr)->_schedpolicy = policy; return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 5c5e64d18c..f939a865e6 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -6,6 +6,9 @@ namespace { +constexpr int POSIX_PROCESS_PRIVATE = 0; +constexpr int POSIX_PROCESS_SHARED = 1; + int toPosix(int result) { if (result == 0) return 0; @@ -64,4 +67,11 @@ int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, Kerne return toPosix(scePthreadCondattrSetclock(attr, clock_id)); } +int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE && pshared != POSIX_PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + (*attr)->_pshared = pshared; + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index 7ab97118fb..fb378b0a8d 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -12,6 +12,8 @@ namespace { constexpr int POSIX_EINVAL = 22; +constexpr int POSIX_PROCESS_PRIVATE = 0; +constexpr int POSIX_PROCESS_SHARED = 1; constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int POSIX_PRIO_PROTECT = 2; constexpr int POSIX_SCHED_RR = 3; @@ -60,7 +62,7 @@ int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex) { } int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_mutex_timedlock: null abstime"); + if (!abstime) return pthread_mutex_lock_nid_postfix(mutex); _initializeStatic(mutex, __func__); if (scePthreadMutexTrylock(mutex) == 0) return 0; KernelUseconds usec = 0; @@ -88,8 +90,22 @@ int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr) { return PosixThread::ToErrno(result); } +int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared) { + if (!attr || !*attr) return POSIX_EINVAL; + if (!pshared) throw std::runtime_error("pthread_mutexattr_getpshared: null pshared"); + *pshared = (*attr)->pshared; + return 0; +} + +int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared) { + if (!attr || !*attr) return POSIX_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE && pshared != POSIX_PROCESS_SHARED) return POSIX_EINVAL; + (*attr)->pshared = pshared; + return 0; +} + int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol) { - if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_setprotocol: null attr"); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if (protocol < 0 || protocol > POSIX_PRIO_PROTECT) return POSIX_EINVAL; (*attr)->protocol = protocol; (*attr)->ceiling = sched_get_priority_max_nid_postfix(POSIX_SCHED_RR); @@ -97,14 +113,14 @@ int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, } int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling) { - if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_setprioceiling: null attr"); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if ((*attr)->protocol != POSIX_PRIO_PROTECT) return POSIX_EINVAL; (*attr)->ceiling = prioceiling; return 0; } int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling) { - if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_getprioceiling: null attr"); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if (!prioceiling) throw std::runtime_error("pthread_mutexattr_getprioceiling: null prioceiling"); if ((*attr)->protocol != POSIX_PRIO_PROTECT) return POSIX_EINVAL; *prioceiling = (*attr)->ceiling; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index b64d1359b1..1faef35844 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -36,6 +36,8 @@ void initializeStatic(PthreadRwlock* rwlock, const char* funcName) { extern "C" { +int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); + int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock) { if (!rwlock) throw std::runtime_error("pthread_rwlock_destroy: null rwlock"); if (*rwlock == nullptr) return 0; @@ -60,7 +62,7 @@ int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { } int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_rwlock_timedrdlock: null abstime"); + if (!abstime) return pthread_rwlock_rdlock_nid_postfix(rwlock); initializeStatic(rwlock, __func__); if (scePthreadRwlockTryrdlock(rwlock) == 0) return 0; KernelUseconds usec = 0; @@ -69,7 +71,7 @@ int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, cons } int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_rwlock_timedwrlock: null abstime"); + if (!abstime) return pthread_rwlock_wrlock_nid_postfix(rwlock); initializeStatic(rwlock, __func__); if (scePthreadRwlockTrywrlock(rwlock) == 0) return 0; KernelUseconds usec = 0; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index d11bda07ad..e780d7f59f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,7 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + ThreadCancel::Check(); { std::lock_guard lock(semaphore->lock); if (semaphore->count > 0) { diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 7660915456..c2e3d1aaee 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -61,11 +61,10 @@ int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); if (!mask) return fail(GuestEfault); auto* self = scePthreadSelf(); + KernelCpumask affinity = 0; 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); - } + const auto tid = id == -1 ? self->tid : id; + if (!GetLiveGuestThreadAffinity_nid_no_patch(tid, affinity)) return fail(GuestEsrch); } else if (which == WhichProcess) { if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); } else if (which >= 3 && which <= 6) { @@ -75,9 +74,7 @@ int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t } 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 (which == WhichThread) scePthreadGetaffinity(self, &affinity); - else affinity = LiveGuestThreadsAffinity(); + if (which == WhichProcess) affinity = LiveGuestThreadsAffinity(); std::memset(mask, 0, size); std::memcpy(mask, &affinity, sizeof(affinity)); return 0; @@ -99,10 +96,7 @@ int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t } 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); - } + if (id != -1 && id != self->tid && !LiveGuestThreadExists(id)) return fail(GuestEsrch); } else if (which == WhichProcess) { if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); } else if (which >= 3 && which <= 6) { @@ -116,8 +110,10 @@ int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t std::memcpy(&requested, bytes, sizeof(requested)); const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); if (affinity == 0) return fail(GuestEdeadlk); - if (which == WhichThread) scePthreadSetaffinity(self, affinity); - else SetLiveGuestThreadsAffinity(affinity); + if (which == WhichThread) { + const auto tid = id == -1 ? self->tid : id; + if (!SetLiveGuestThreadAffinity_nid_no_patch(tid, affinity)) return fail(GuestEsrch); + } else SetLiveGuestThreadsAffinity(affinity); return 0; } @@ -237,6 +233,9 @@ int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name) { int APS5_VABI pthread_getname_np_nid_postfix(Pthread thread, char* name) { if (!thread) return PosixThread::GUEST_ESRCH; if (!name) return PosixThread::GUEST_EFAULT; + // A joined thread answers ESRCH on the console (PS5 firmware 13.60); the object is retained + // after scePthreadJoin, so _joined tells it apart from a live thread. + if (thread->_joined.load(std::memory_order_acquire)) return PosixThread::GUEST_ESRCH; return PosixThread::ToErrno(scePthreadGetname(thread, name)); } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index e22426757f..9b049a94f5 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -32,6 +32,7 @@ struct PthreadMutexattrPrivate { MutexType type; int protocol = 0; int ceiling = 0; + int pshared = 0; }; struct PthreadMutexPrivate { @@ -58,6 +59,7 @@ struct PthreadRwlockPrivate { struct PthreadCondattrPrivate { int _clockid; + int _pshared = 0; }; struct PthreadCondPrivate { @@ -139,6 +141,8 @@ struct PthreadPrivate { void* _retval; bool _detached; bool _adopted; + Pthread* _handleStorage; + std::atomic _joined; std::mutex _join_mtx; TimedWait::Condition _join_cv; std::atomic cancelPending{false}; @@ -160,5 +164,7 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); +bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); +bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); #endif diff --git a/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp b/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp index 2a42b292f4..1cd1f7ca70 100644 --- a/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp +++ b/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp @@ -8,6 +8,7 @@ template class WindowsThreadLocal { public: static T& Get() { + const DWORD slot = slotIndex(); if (slot == FLS_OUT_OF_INDEXES) throw std::runtime_error("Cannot allocate native thread state slot"); if (auto* value = static_cast(FlsGetValue(slot))) return *value; auto value = std::make_unique(); @@ -17,10 +18,13 @@ public: private: static void WINAPI Destroy(void* value) { - FlsSetValue(slot, value); + FlsSetValue(slotIndex(), value); delete static_cast(value); - FlsSetValue(slot, nullptr); + FlsSetValue(slotIndex(), nullptr); + } + static DWORD slotIndex() { + static const DWORD slot = FlsAlloc(Destroy); + return slot; } - inline static const DWORD slot = FlsAlloc(Destroy); }; #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 9d2f032743..4102a9e69e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -68,8 +68,7 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz } int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, std::size_t* stacksize) { - if (!attr || !*attr || !stackaddr || !stacksize) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr || !stackaddr || !stacksize) return SCE_KERNEL_ERROR_EINVAL; *stackaddr = (*attr)->stackAddress; *stacksize = (*attr)->_stacksize; return SCE_OK; @@ -104,8 +103,7 @@ int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) } int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!guard_size) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !guard_size) return SCE_KERNEL_ERROR_EINVAL; *guard_size = (*attr)->_guardsize; return SCE_OK; } @@ -124,8 +122,7 @@ int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched } int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!stack_addr) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !stack_addr) return SCE_KERNEL_ERROR_EINVAL; *stack_addr = (*attr)->stackAddress; return SCE_OK; } @@ -144,7 +141,7 @@ int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { } int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, size_t guard_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_guardsize = guard_size; return SCE_OK; } @@ -176,15 +173,14 @@ int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { } int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* addr, size_t size) { - if (!attr || !*attr || !addr) throw std::runtime_error(std::string(__func__) + ": null arg"); - if (size < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); + if (!attr || !*attr || !addr || size < MIN_STACK_SIZE) return SCE_KERNEL_ERROR_EINVAL; (*attr)->stackAddress = addr; (*attr)->_stacksize = size; return SCE_OK; } int APS5_VABI scePthreadAttrSetstackaddr(PthreadAttr* attr, void* addr) { - if (!attr || !*attr || !addr) throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr || !addr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->stackAddress = addr; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index d576c64ff0..f0686f555d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -130,16 +130,14 @@ int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr) { } int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr) { - if (!attr || !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return 0; } int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId) { - if (!attr || !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_clockid = static_cast(clockId); return 0; } @@ -147,10 +145,8 @@ int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid cl int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char*) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - if (attr && !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); auto replacement = std::make_unique(); - if (attr) + if (attr && *attr) replacement->_clockid = (*attr)->_clockid; std::lock_guard lock(condInitializationMutex); std::atomic_ref(*cond).store(replacement.release(), std::memory_order_release); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index b7e180b1cf..b27a79bb5e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -303,7 +303,7 @@ static CpuClockRegistry& CpuClocks() { return *registry; } -PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false), _retval(nullptr), _detached(false), _adopted(false) { +PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false), _retval(nullptr), _detached(false), _adopted(false), _handleStorage(nullptr), _joined(false) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); const auto* parent = CurrentGuestState().current; @@ -375,6 +375,28 @@ bool GuestThreadIsLive(const PthreadPrivate* thread) { return false; } +bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + mask = thread->affinity.load(std::memory_order_relaxed); + return true; + } + return false; +} + +bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + thread->affinity.store(mask, std::memory_order_relaxed); + return true; + } + return false; +} + bool LiveGuestThreadExists(std::int64_t tid) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); @@ -434,7 +456,7 @@ static void* MeasureStackReserve(void* reserve) { std::uintptr_t low = 0; std::uintptr_t high = 0; if (HostStackLimits(&low, &high)) - *static_cast(reserve) = high - reinterpret_cast(&low); + *static_cast(reserve) = high - reinterpret_cast(__builtin_frame_address(0)); return nullptr; } @@ -557,6 +579,26 @@ static void ReleaseThread(PthreadPrivate* thread) { delete thread; } +static std::mutex joinedLock; +static std::map joinedThreads; + +static void RetainJoinedThread(PthreadPrivate* thread) { + std::lock_guard lock(joinedLock); + Pthread* const storage = thread->_handleStorage; + const auto found = joinedThreads.find(storage); + if (found != joinedThreads.end() && found->second != thread) ReleaseThread(found->second); + joinedThreads[storage] = thread; +} + +static void RecycleJoinedThread(Pthread* storage) { + std::lock_guard lock(joinedLock); + const auto found = joinedThreads.find(storage); + if (found == joinedThreads.end()) return; + PthreadPrivate* joined = found->second; + joinedThreads.erase(found); + ReleaseThread(joined); +} + #ifdef _WIN32 // The title's job workers ("BPE JobWorkerThread CPU0".."CPU12") spin at full load on one core each. // Pinning them to the efficiency cores of a hybrid CPU (APS5_JOB_AFFINITY=1, with the driver's @@ -718,8 +760,10 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread throw; } auto* published = p.release(); + published->_handleStorage = thread; *thread = published; start.set_value(true); + RecycleJoinedThread(thread); if (detached) ReleaseThread(published); #else @@ -774,8 +818,10 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread throw; } auto* published = p.release(); + published->_handleStorage = thread; *thread = published; start.set_value(true); + RecycleJoinedThread(thread); if (detached) ReleaseThread(published); #endif @@ -785,30 +831,32 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; ThreadCancel::Check(); { std::unique_lock lock(thread->_join_mtx); ThreadCancel::WaitUntil(thread->_join_cv, lock, std::nullopt, [&] { return thread->_finished.load(); }); } + if (thread->_joined.exchange(true, std::memory_order_acq_rel)) return SCE_KERNEL_ERROR_ESRCH; #ifdef _WIN32 if (WaitForSingleObject(thread->nativeHandle, INFINITE) != WAIT_OBJECT_0) throw std::system_error(GetLastError(), std::system_category(), "Joining guest thread"); if (retval) *retval = thread->_retval; - ReleaseThread(thread); #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 + RetainJoinedThread(thread); return SCE_OK; } int APS5_VABI scePthreadDetach(Pthread thread) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_EINVAL; thread->_detached = true; #ifdef _WIN32 ReleaseThread(thread); diff --git a/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp b/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp new file mode 100644 index 0000000000..51ebf73520 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp @@ -0,0 +1,6 @@ +#pragma once +#ifdef _WIN32 +#include + +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address); +#endif diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 4597fbff2d..c81d93d46b 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -7,6 +7,8 @@ struct SocketOptionState { int reportedError = 0; bool nonblocking = false; bool noSignal = false; + std::int64_t sendTimeout = 0; + std::int64_t receiveTimeout = 0; }; int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index a39295ecd6..5b22edcf5d 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -16,6 +16,8 @@ int Close(int descriptor); void Forget(int descriptor); bool IsOpen(int descriptor); int Family(int descriptor); +int Type(int descriptor); +bool Listening(int descriptor); std::int64_t Read(int descriptor, void* buffer, std::size_t length); std::int64_t Write(int descriptor, const void* buffer, std::size_t length); int Stat(int descriptor, FileStat* status); @@ -31,6 +33,7 @@ struct Interest { std::uintptr_t CurrentWaker(); void Wake(std::uintptr_t waker); bool WaitAny(const std::vector& interests, std::uint64_t deadlineNanos); +bool PipeReadState(int descriptor, std::int64_t* data, bool* eof); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); } diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 8500256e25..c4adb38dfd 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -110,7 +110,7 @@ int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMillise if (entry.revents != 0) ++ready; } if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(1))); } } diff --git a/core/libs/prx/libkernel/Socket/src/SocketConnect.cpp b/core/libs/prx/libkernel/Socket/src/SocketConnect.cpp new file mode 100644 index 0000000000..245fd5e527 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/SocketConnect.cpp @@ -0,0 +1,47 @@ +#ifdef _WIN32 +#include "prx/libkernel/Socket/include/SocketConnect.hpp" +#include +#include +#include +#include + +namespace { + +bool IsLoopback(const sockaddr* address) { + if (address->sa_family == AF_INET) return (ntohl(reinterpret_cast(address)->sin_addr.s_addr) >> 24) == 127; + if (address->sa_family == AF_INET6) return IN6_IS_ADDR_LOOPBACK(&reinterpret_cast(address)->sin6_addr); + return false; +} + +u_short Port(const sockaddr* address) { + return address->sa_family == AF_INET ? reinterpret_cast(address)->sin_port : reinterpret_cast(address)->sin6_port; +} + +template +bool ListenerOnPort(ULONG family, u_short port) { + std::vector table; + ULONG size = 0; + DWORD result = ERROR_INSUFFICIENT_BUFFER; + while (result == ERROR_INSUFFICIENT_BUFFER) { + table.resize(size); + result = GetExtendedTcpTable(table.data(), &size, FALSE, family, TCP_TABLE_OWNER_PID_LISTENER, 0); + } + if (result != NO_ERROR) return true; + const auto* rows = reinterpret_cast(table.data()); + for (DWORD i = 0; i < rows->dwNumEntries; ++i) { + if (static_cast(rows->table[i].dwLocalPort) == port) return true; + } + return false; +} + +} + +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address) { + if (!IsLoopback(address)) return; + const u_short port = Port(address); + if (ListenerOnPort(AF_INET, port) || ListenerOnPort(AF_INET6, port)) return; + TCP_INITIAL_RTO_PARAMETERS parameters{TCP_INITIAL_RTO_UNSPECIFIED_RTT, TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS}; + DWORD returned = 0; + WSAIoctl(socket, SIO_TCP_INITIAL_RTO, ¶meters, sizeof(parameters), nullptr, 0, &returned, nullptr, nullptr); +} +#endif diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 78f7720167..184219224d 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -26,6 +26,8 @@ constexpr int GuestOobInline = 0x100; constexpr int GuestNoSigpipe = 0x800; constexpr int GuestSendBuffer = 0x1001; constexpr int GuestReceiveBuffer = 0x1002; +constexpr int GuestSendTimeout = 0x1005; +constexpr int GuestReceiveTimeout = 0x1006; constexpr int GuestError = 0x1007; constexpr int GuestType = 0x1008; constexpr int GuestNonBlocking = 0x1200; @@ -36,12 +38,27 @@ constexpr int ErrorInvalid = 22; constexpr int ErrorDomain = 33; constexpr int ErrorNoProtocolOption = 42; constexpr int MaximumLinger = 65535; +constexpr std::int64_t LongestTimeout = 0x7fffffffffffffffLL; struct GuestLingerValue { std::int32_t enabled; std::int32_t seconds; }; +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +std::int64_t TimeoutOf(const GuestTimeval& time) { + if (time.seconds > INT32_MAX) return LongestTimeout; + return (time.seconds << 32) + static_cast((static_cast(time.microseconds) * ((std::uint64_t{1} << 63) / 500000)) >> 32); +} + +GuestTimeval TimevalOf(std::int64_t timeout) { + return {timeout >> 32, (1000000 * static_cast(static_cast(timeout))) >> 32}; +} + #ifdef _WIN32 using NativeHandle = SOCKET; using NativeOptionValue = const char*; @@ -95,6 +112,15 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op if (::setsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(&request), sizeof(request))) return HostError(); return 0; } + if (option == GuestSendTimeout || option == GuestReceiveTimeout) { + if (length < sizeof(GuestTimeval)) return ErrorInvalid; + if (!value) return ErrorInvalid; + GuestTimeval time; + std::memcpy(&time, value, sizeof(time)); + if (time.seconds < 0 || time.microseconds < 0 || time.microseconds >= 1000000) return ErrorDomain; + (option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout) = TimeoutOf(time); + return 0; + } if (option == GuestNoSigpipe || option == GuestNonBlocking) { if (length < sizeof(int)) return ErrorInvalid; if (!value) return ErrorInvalid; @@ -142,6 +168,10 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty const GuestLingerValue guest{current.l_onoff != 0 ? GuestLinger : 0, static_cast(current.l_linger)}; return Copy(value, length, &guest, sizeof(guest)); } + if (option == GuestSendTimeout || option == GuestReceiveTimeout) { + const GuestTimeval time = TimevalOf(option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout); + return Copy(value, length, &time, sizeof(time)); + } int number = 0; if (const int nativeOption = NativeBoolean(option); nativeOption != -1) { int enabled = 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index f91667d212..9bb4ed1b3e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -7,6 +7,9 @@ #else #include #include +#ifdef __linux__ +#include +#endif #include #include #include @@ -28,6 +31,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketConnect.hpp" #include #include #include @@ -42,7 +46,6 @@ #include #include #include -#include #include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -122,6 +125,10 @@ struct Socket { struct Waker { NativeSocket value = Invalid; Waker() { +#ifdef _WIN32 + static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); + if (startup) throw std::runtime_error("equeue waker: WSAStartup failed, error " + std::to_string(startup)); +#endif value = ::socket(AF_INET, SOCK_DGRAM, 0); sockaddr_in address{}; address.sin_family = AF_INET; @@ -243,6 +250,11 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } +bool Listening(const Socket& socket) { + int listening = 0; + socklen_t size = sizeof(listening); + return ::getsockopt(socket.value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size) == 0 && listening != 0; +} int Disconnect(const Socket& socket) { sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); @@ -335,6 +347,7 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t std::vector entries; constexpr SHORT ReadMask = POLLRDNORM; constexpr SHORT WriteMask = POLLWRNORM; + bool pipes = false; #else std::vector entries; constexpr short ReadMask = POLLIN; @@ -344,7 +357,14 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t std::lock_guard lock(socketsMutex); for (const auto& interest : interests) { const auto found = sockets.find(interest.descriptor); - if (found == sockets.end()) continue; + if (found == sockets.end()) { +#ifdef _WIN32 + pipes = true; +#else + entries.push_back({interest.descriptor, ReadMask, 0}); +#endif + continue; + } held.push_back(found->second); entries.push_back({found->second->value, interest.write ? WriteMask : ReadMask, 0}); } @@ -362,6 +382,7 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t if (remaining < 1000000) return false; timeout = static_cast(std::min(remaining / 1000000, INT_MAX)); } + if (pipes && (timeout < 0 || timeout > 1)) timeout = 1; if (WSAPoll(entries.data(), static_cast(entries.size()), timeout) == SOCKET_ERROR) throw std::runtime_error("equeue wait: WSAPoll failed, guest errno " + std::to_string(NativeError())); #else @@ -383,6 +404,23 @@ int GuestSockets::Family(int descriptor) { const auto found = sockets.find(descriptor); return found == sockets.end() ? -1 : found->second->family; } +int GuestSockets::Type(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + return found == sockets.end() ? -1 : found->second->type; +} +bool GuestSockets::Listening(int descriptor) { + std::shared_ptr socket; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return false; + socket = found->second; + } + int accepting = 0; + socklen_t length = sizeof(accepting); + return getsockopt(socket->value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &length) == 0 && accepting != 0; +} extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { return GuestSockets::IsOpen(descriptor); @@ -459,18 +497,22 @@ constexpr int FileAccessInformationClass = 8; constexpr int FilePipeLocalInformationClass = 24; constexpr ULONG PipeClosingState = 4; constexpr ULONG PipeAtomicBytes = 512; -bool PipeReadiness(int descriptor, short events, short& revents) { +bool QueryPipe(int descriptor, const char* function, ACCESS_MASK& access, PipeLocalInformation& pipe) { 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"); + if (!query) throw std::runtime_error(std::string(function) + ": 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"); + throw std::runtime_error(std::string(function) + ": querying pipe descriptor " + std::to_string(descriptor) + " failed"); + return true; +} +bool PipeReadiness(int descriptor, short events, short& revents) { + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (!QueryPipe(descriptor, "poll", access, pipe)) return false; if ((events & GuestPollWritable) && !(access & FILE_WRITE_DATA)) ThrowReadEndWritable(descriptor); const bool closed = pipe.state == PipeClosingState; revents = 0; @@ -563,6 +605,34 @@ int SocketSendFlags(Socket& socket, int flags) { std::lock_guard lock(socket.modeMutex); return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } +int WaitForTransfer(Socket& socket, bool send, int flags) { + if ((flags & GuestDontWait) != 0) return 0; + std::int64_t timeout; + { + std::lock_guard lock(socket.modeMutex); + if (socket.options.nonblocking) return 0; + timeout = send ? socket.options.sendTimeout : socket.options.receiveTimeout; + } + if (timeout == 0) return 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout >> 32) + + std::chrono::nanoseconds((static_cast(static_cast(timeout)) * 1000000000) >> 32); + for (;;) { + const auto left = deadline - std::chrono::steady_clock::now(); + if (left <= std::chrono::steady_clock::duration::zero()) return 35; + const int milliseconds = static_cast(std::min(std::chrono::ceil(left).count(), INT_MAX)); + NativePollDescriptor entry{}; + entry.fd = socket.value; +#ifdef _WIN32 + entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); + const int ready = WSAPoll(&entry, 1, milliseconds); + if (ready != 0) return 0; +#else + entry.events = static_cast(send ? POLLOUT : POLLIN); + const int ready = ::poll(&entry, 1, milliseconds); + if (ready > 0 || (ready < 0 && errno != EINTR)) return 0; +#endif + } +} #ifdef _WIN32 bool LoopbackName(NativeSocket socket, sockaddr_in& name) { int size = sizeof(name); @@ -673,7 +743,7 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); - if (found == sockets.end()) return false; + if (found == sockets.end()) return !write && PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); socket = found->second; } #ifdef _WIN32 @@ -690,6 +760,10 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e int size = 0; socklen_t length = sizeof(size); if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&size), &length) == 0) *data = size; +#ifdef __linux__ + int queued = 0; + if (*data > 0 && ::ioctl(socket->value, SIOCOUTQ, &queued) == 0) *data = std::max(0, *data - queued); +#endif } else { #ifdef _WIN32 unsigned long available = 0; @@ -702,6 +776,25 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e return true; } +bool GuestSockets::PipeReadState(int descriptor, std::int64_t* data, bool* eof) { +#ifdef _WIN32 + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (!QueryPipe(descriptor, "kevent", access, pipe) || !(access & FILE_READ_DATA)) return false; + *data = pipe.readDataAvailable; + *eof = pipe.state == PipeClosingState; +#else + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_WRONLY) return false; + pollfd entry{descriptor, POLLIN, 0}; + int available = 0; + if (::poll(&entry, 1, 0) < 0 || ::ioctl(descriptor, FIONREAD, &available) != 0) + throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + *data = available; + *eof = (entry.revents & POLLHUP) != 0; +#endif + return true; +} + int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector> held; std::vector native; @@ -855,7 +948,9 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { if (family != 2 && family != 28) return Fail(47); const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; const int expectedProtocol = plainType == 1 ? 6 : 17; - if (nativeType == -1 || (protocol != 0 && protocol != expectedProtocol)) return Fail(43); + const bool inetKnowsType = plainType == 3 || plainType == 5; + if (nativeType == -1) return Fail(protocol == 0 && plainType != 0 && !inetKnowsType ? 41 : 43); + if (protocol != 0 && protocol != expectedProtocol) return Fail(43); #ifdef _WIN32 static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); if (startup) return Fail(5); @@ -928,6 +1023,10 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; +#ifdef _WIN32 + if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); +#endif + if (socket->type == 1 && Listening(*socket)) return Fail(45); if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { std::lock_guard lock(socket->modeMutex); @@ -959,6 +1058,8 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::lock_guard modeLock(listener->modeMutex); inheritNonblocking = listener->options.nonblocking; accepted->options.noSignal = listener->options.noSignal; + accepted->options.sendTimeout = listener->options.sendTimeout; + accepted->options.receiveTimeout = listener->options.receiveTimeout; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; const int acceptedDescriptor = Install(std::move(accepted)); @@ -975,6 +1076,7 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); return result < 0 ? Fail(SendError(socket->type)) : result; } @@ -986,6 +1088,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!buffer && length) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); if (!destination.Open()) return Fail(14); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), NativeReceiveFlags(flags)); #ifdef _WIN32 @@ -1072,6 +1175,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); int result; if (!address) { if (addressLength != 0) return Fail(22); @@ -1097,6 +1201,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if (!destination.Open()) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), NativeReceiveFlags(flags), reinterpret_cast(&native), &size); #ifdef _WIN32 @@ -1172,6 +1277,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, } sockaddr_storage native{}; socklen_t size = sizeof(native); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), NativeReceiveFlags(flags), reinterpret_cast(&native), &size); if (received < 0) { @@ -1378,8 +1484,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c 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)); + } else if (!ready && !pipeReady) { + const auto wake = std::chrono::steady_clock::now() + std::chrono::milliseconds(1); + PreciseSleepUntil(timeout < 0 ? wake : std::min(deadline, wake)); } int socketReady = 0; for (const auto& request : native) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 178c6459e0..50b6eb5d26 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -158,6 +158,11 @@ int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::option if (timeArgument == nullptr) { return 0; } + if (sizeArgument == nullptr) { + const auto usecs = *static_cast(timeArgument); + deadlineNanos = TimedWait::DeadlineNanos(usecs); + return 0; + } UmtxTime time{{0, 0}, 0, GUEST_CLOCK_REALTIME}; if (reinterpret_cast(sizeArgument) <= sizeof(KernelTimespec)) { time.timeout = *static_cast(timeArgument); @@ -165,9 +170,6 @@ int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::option time = *static_cast(timeArgument); } if (time.timeout.tv_sec < 0 || time.timeout.tv_nsec < 0 || time.timeout.tv_nsec >= NANOS_PER_SECOND) { - if (sizeArgument == nullptr) { - NotImplemented_nid_no_patch("_umtx_op timeout with size argument 0 that is not a valid timespec"); - } return GUEST_EINVAL; } diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 1a42cca261..a644a630f1 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -93,13 +93,18 @@ int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { } int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { - (void)threadContextInfo; - (void)threadContextInfoSize; + const auto address = reinterpret_cast(threadContextInfo); + if (threadContextInfo == nullptr || threadContextInfoSize != 144 || address % alignof(uint64_t) != 0 || threadContextInfoSize > UINTPTR_MAX - address) + throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported thread context buffer outside the core dump handler"); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +int APS5_VABI sceCoredumpWriteUserString() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCoredumpWriteUserString() { +int APS5_VABI sceCoredumpConfigDumpMode() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index d16c8c6055..1bca3dd663 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -9,6 +9,10 @@ #include #include #endif +#if defined(__linux__) && defined(__x86_64__) +#include +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -31,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -179,6 +184,11 @@ struct GuestResourceLimit { std::int64_t rlim_max; }; +namespace { +std::mutex openFileLimitMutex; +std::optional guestOpenFileLimit; +} + struct GuestResourceUsage { KernelTimeval ru_utime; KernelTimeval ru_stime; @@ -340,6 +350,13 @@ int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limi GuestResourceLimit current{}; getrlimit_nid_postfix(resource, ¤t); + if (resource == guestRlimitNofile) { + if (requested.rlim_max <= current.rlim_max && requested.rlim_cur <= requested.rlim_max) { + std::lock_guard lock(openFileLimitMutex); + guestOpenFileLimit = requested; + return 0; + } + } 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; @@ -452,15 +469,52 @@ int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { return 256; } +int APS5_VABI sysarch(int operation, void* parameters) { + constexpr int getFsBase = 128; + constexpr int getGsBase = 130; + constexpr int setGsBase = 131; + constexpr std::uint64_t userAddressEnd = 0x0000800000000000ull; + if (operation != getFsBase && operation != getGsBase && operation != setGsBase) + NotImplemented_nid_no_patch(("sysarch: operation " + std::to_string(operation)).c_str()); +#if defined(__linux__) && defined(__x86_64__) + if (parameters == nullptr) { + *__error_nid_postfix() = guestFault; + return -1; + } + std::uint64_t base = 0; + if (operation == setGsBase) { + std::memcpy(&base, parameters, sizeof(base)); + if (base >= userAddressEnd) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + if (::syscall(SYS_arch_prctl, ARCH_SET_GS, base) != 0) throw std::system_error(errno, std::generic_category(), "sysarch: setting the GS base"); + return 0; + } + if (::syscall(SYS_arch_prctl, operation == getFsBase ? ARCH_GET_FS : ARCH_GET_GS, &base) != 0) + throw std::system_error(errno, std::generic_category(), "sysarch: reading a segment base"); + std::memcpy(parameters, &base, sizeof(base)); + return 0; +#else + static_cast(parameters); + NotImplemented_nid_no_patch("sysarch: segment bases on this host"); + return 0; +#endif +} + int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { - if (usage == nullptr) - throw std::invalid_argument("getrusage: usage is null"); + if (who != -1 && who != 0 && who != 1) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + if (usage == nullptr) { + *__error_nid_postfix() = guestFault; + return -1; + } if (who == -1) { *usage = {}; return 0; } - if (who != 0 && who != 1) - throw std::invalid_argument("getrusage: unsupported who"); #ifdef _WIN32 FILETIME creation{}; FILETIME exitTime{}; @@ -547,17 +601,19 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { return 0; } case guestRlimitNofile: { + std::lock_guard lock(openFileLimitMutex); + if (!guestOpenFileLimit) { #ifdef _WIN32 - constexpr std::int64_t crtDescriptors = 8192; - limit->rlim_cur = crtDescriptors; - limit->rlim_max = crtDescriptors; + constexpr std::int64_t crtDescriptors = 8192; + guestOpenFileLimit = GuestResourceLimit{crtDescriptors, crtDescriptors}; #else - rlimit native{}; - if (::getrlimit(RLIMIT_NOFILE, &native) != 0) - throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); - limit->rlim_cur = toGuestLimit(native.rlim_cur); - limit->rlim_max = toGuestLimit(native.rlim_max); + rlimit native{}; + if (::getrlimit(RLIMIT_NOFILE, &native) != 0) + throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); + guestOpenFileLimit = GuestResourceLimit{toGuestLimit(native.rlim_cur), toGuestLimit(native.rlim_max)}; #endif + } + *limit = *guestOpenFileLimit; return 0; } case guestRlimitCpu: diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 1437144b9e..2547ebc59c 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -42,6 +42,7 @@ constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; +std::atomic blockedMaskExtra[3]{}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; @@ -77,7 +78,12 @@ 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; } +bool SignalBlocked(int guest) { + if (guest < 1 || guest > MaxSignal) return false; + const auto word = SignalWord(guest); + const auto mask = word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load(); + return (mask & SignalBit(guest)) != 0; +} struct PendingSignals { std::mutex lock; @@ -282,7 +288,7 @@ void ExpireAlarm(std::chrono::microseconds interval) { handler = dispositions[GuestSigalrm].handler; } if (handler == 1) return; - if ((blockedMask.load() & (1u << (GuestSigalrm - 1))) != 0) + if (SignalBlocked(GuestSigalrm)) 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"); @@ -353,11 +359,14 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output *__error_nid_postfix() = 22; return -1; } - if (which != 0) throw std::runtime_error("getitimer: CPU interval timers are not implemented"); if (output == nullptr) { *__error_nid_postfix() = 14; return -1; } + if (which != 0) { + *output = {}; + return 0; + } GuestItimerval result{}; auto& alarms = Alarms(); { @@ -524,15 +533,22 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest 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; + for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); } 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; + case 1: + blockedMask.fetch_or(set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + break; + case 3: + blockedMask.store(set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + break; default: throw std::invalid_argument("_sigprocmask: invalid how"); } } @@ -596,6 +612,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { { std::lock_guard lock(registration); previous.bits[0] = blockedMask.exchange(set->bits[0]); + for (int word = 1; word < 4; ++word) previous.bits[word] = blockedMaskExtra[word - 1].exchange(set->bits[word]); } WaitForDeliverableSignal(); _sigprocmask_nid_postfix(GuestSigsetmask, &previous, nullptr); @@ -608,9 +625,8 @@ int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { 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; + for (int word = 1; word < 4; ++word) + set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & blockedMaskExtra[word - 1].load(); return 0; } } diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 5803d5300a..3205d75fce 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -44,7 +44,7 @@ int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* ol } bool IsLeafMib(const int* name) { - return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5)) || (name[0] == 1 && name[1] == 37); + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); } } @@ -100,6 +100,14 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } + if (nameLength == 2 && name[0] == 6 && name[1] == 7) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + constexpr int pagesize = PS5_PAGE_SIZE; + return CopyOut(&pagesize, sizeof(pagesize), oldValue, oldLength); + } if (nameLength == 2 && name[0] == 6 && name[1] == 3) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -219,6 +227,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si const std::uint64_t frequency = sceKernelGetTscFrequency(); return CopyOut(&frequency, sizeof(frequency), oldValue, oldLength); } + if (std::strcmp(name, "hw.pagesize") == 0) { + static constexpr int mib[] = {6, 7}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } if (std::strcmp(name, "hw.ncpu") == 0) { static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); @@ -257,6 +269,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ static constexpr KnownName known[] = { {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, + {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index e25277e821..059c0236f7 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -348,10 +348,7 @@ static std::uint64_t ProcessCpuResolutionNanos() { } #endif -int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { - if (tp == nullptr) { - APS5_INVALID_ARG_EX; - } +static int ReadClock(int clockId, KernelTimespec* tp) { if (IsCpuClock(clockId)) { std::uint64_t nanos = 0; if (!GuestThreadCpuNanos(static_cast(static_cast(clockId) & CPU_CLOCK_ID_MASK), &nanos)) { @@ -401,7 +398,7 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; } - throw std::runtime_error(std::string(__func__) + ": unsupported clock_id " + std::to_string(clockId)); + throw std::runtime_error(std::string("clock_gettime") + ": unsupported clock_id " + std::to_string(clockId)); #else clockid_t nativeId; switch (clockId) { @@ -434,7 +431,7 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { nativeId = CLOCK_PROCESS_CPUTIME_ID; break; default: - throw std::runtime_error(std::string(__func__) + ": unsupported clock_id " + std::to_string(clockId)); + throw std::runtime_error(std::string("clock_gettime") + ": unsupported clock_id " + std::to_string(clockId)); } struct timespec ts{}; if (clock_gettime(nativeId, &ts) != 0) { @@ -446,6 +443,17 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { #endif } +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { + KernelTimespec value{}; + if (ReadClock(clockId, &value) != 0) return -1; + if (tp == nullptr) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + *tp = value; + return 0; +} + int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { KernelTimeval ignoredTime{}; if (tv == nullptr) tv = &ignoredTime; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 99809d6212..481ff4b823 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -491,9 +491,14 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) 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 __APPLE__ + if (::fchmodat(AT_FDCWD, native.c_str(), static_cast(mode & 07777), AT_SYMLINK_NOFOLLOW) != 0) { + return static_cast(Fail(GuestFileError(errno))); } +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; + } } #ifdef _WIN32 if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(GuestFileError(errno))); diff --git a/core/libs/tests/AppArchiveFixture.hpp b/core/libs/tests/AppArchiveFixture.hpp new file mode 100644 index 0000000000..b6c5e4529c --- /dev/null +++ b/core/libs/tests/AppArchiveFixture.hpp @@ -0,0 +1,53 @@ +#ifndef CORE_LIBS_TESTS_APPARCHIVEFIXTURE_HPP +#define CORE_LIBS_TESTS_APPARCHIVEFIXTURE_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace AppArchiveFixture { + +inline std::vector Pattern(std::size_t size) { + std::vector data(size); + for (std::size_t i = 0; i < size; ++i) data[i] = static_cast((i * 7 + i / 4096) & 0xff); + return data; +} + +inline std::filesystem::path EnterFreshRoot(const char* name) { + const auto root = std::filesystem::temp_directory_path() / + (std::string(name) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::filesystem::current_path(root); + return std::filesystem::canonical(root); +} + +inline void WriteArchive(const std::filesystem::path& path, const std::map>& files, const std::vector& directories = {}) { + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) std::abort(); + ZArchiveWriter writer( + [](std::int32_t, void*) {}, + [](const void* data, std::size_t length, void* context) { + static_cast(context)->write(static_cast(data), static_cast(length)); + }, + &output); + for (const auto& directory : directories) if (!writer.MakeDir(directory.c_str(), true)) std::abort(); + for (const auto& [name, data] : files) { + const auto slash = name.find_last_of('/'); + if (slash != std::string::npos) writer.MakeDir(name.substr(0, slash).c_str(), true); + if (!writer.StartNewFile(name.c_str())) std::abort(); + if (!data.empty()) writer.AppendData(data.data(), data.size()); + } + writer.Finalize(); + output.close(); + if (!output) std::abort(); +} + +} + +#endif diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index a4c2856b60..f8a530d3b5 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -36,6 +36,8 @@ char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); int* APS5_VABI __error_nid_postfix(); int APS5_VABI atexit_nid_postfix(void (APS5_VABI*)()); +void* APS5_VABI scriptingGetMem_nid_postfix(std::size_t, std::size_t); +void APS5_VABI scriptingFreeMem_nid_postfix(void*); } namespace { @@ -373,7 +375,7 @@ int main(int argc, char** argv) { require(directory == reinterpret_cast(storage.data()) && directory[0] == '/' && lastSize == std::strlen(directory) + 1); ApplicationHeapFree_nid_no_patch(directory); directory = getcwd_nid_postfix(nullptr, 200); - require(directory == reinterpret_cast(storage.data()) && lastSize == 200); + require(directory == reinterpret_cast(storage.data()) && lastSize == std::strlen(directory) + 1); ApplicationHeapFree_nid_no_patch(directory); fail = true; *__error_nid_postfix() = 0; @@ -385,4 +387,15 @@ int main(int argc, char** argv) { *__error_nid_postfix() = 0; require(getcwd_nid_postfix(nullptr, 0) == nullptr && *__error_nid_postfix() == 12); fail = false; + const auto beforeScripting = frees; + scriptingFreeMem_nid_postfix(nullptr); + require(frees == beforeScripting); + require(scriptingGetMem_nid_postfix(0, 31) == storage.data() && lastAlignment == 16 && lastSize == 31); + require(scriptingGetMem_nid_postfix(64, 37) == storage.data() && lastAlignment == 64 && lastSize == 37); + scriptingFreeMem_nid_postfix(storage.data()); + require(frees == beforeScripting + 1); + require(scriptingGetMem_nid_postfix(24, 80) == nullptr && lastSize == 37); + fail = true; + require(scriptingGetMem_nid_postfix(16, 64) == nullptr); + fail = false; } diff --git a/core/libs/tests/CrashText.cpp b/core/libs/tests/CrashText.cpp new file mode 100644 index 0000000000..e2076b0c84 --- /dev/null +++ b/core/libs/tests/CrashText.cpp @@ -0,0 +1,56 @@ +#include "prx/libc/include/CrashText.hpp" +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +std::vector Wide(const std::u16string& text) { + std::vector bytes; + for (const char16_t unit : text) { + bytes.push_back(static_cast(unit & 0xff)); + bytes.push_back(static_cast(unit >> 8)); + } + return bytes; +} + +std::vector Narrow(const std::string& text) { + return std::vector(text.begin(), text.end()); +} + +void Expect(const char* name, const std::vector& bytes, const std::string& expected, std::size_t capacity = 960) { + std::vector out(capacity, '#'); + const bool formatted = crash_text::Format(bytes.data(), bytes.size(), out.data(), capacity); + const std::string actual = formatted ? std::string(out.data()) : std::string(""); + if (actual == expected && actual.size() < capacity) return; + std::fprintf(stderr, "%s: expected %s, got %s\n", name, expected.c_str(), actual.c_str()); + ++failures; +} + +} + +int main() { + using namespace std::string_literals; + Expect("terminated UTF-16", Wide(u"LowLevelFatalError [File:Unknown]\0tail"s), "u\"LowLevelFatalError [File:Unknown]\""); + Expect("terminated ASCII with escapes", Narrow("PS5 Out of Memory:\n\t\"a\\b\"\r\0tail"s), "\"PS5 Out of Memory:\\n\\t\\\"a\\\\b\\\"\\r\""); + Expect("UTF-16 up to the end of readable memory", Wide(u"Ran out of memory"), "u\"Ran out of memory\"..."); + Expect("ASCII up to the end of readable memory", Narrow("PAGE END"), "\"PAGE END\"..."); + Expect("UTF-16 before a non-ASCII unit", Wide(u"Memory\u00e9 more"), "u\"Memory\"..."); + Expect("short ASCII", Narrow("abc\0"s), ""); + Expect("short UTF-16", Wide(u"abc\0"s), ""); + Expect("three readable bytes", Narrow("abc"), ""); + Expect("pointer value", std::vector{0x08, 0xd4, 0x8e, 0x5b, 0xf6, 0x7f, 0x00, 0x00}, ""); + Expect("control byte first", Narrow("\x01text\0"s), ""); + Expect("character limit", Wide(std::u16string(600, u'A') + u'\0'), "u\"" + std::string(crash_text::MaximumCharacters, 'A') + "\"..."); + Expect("NUL right at the character limit", Wide(std::u16string(crash_text::MaximumCharacters, u'B') + u'\0'), "u\"" + std::string(crash_text::MaximumCharacters, 'B') + "\""); + Expect("output capacity", Narrow(std::string(100, 'x')), "\"" + std::string(10, 'x') + "\"...", 16); + Expect("escape at the output capacity", Narrow("abcdefghi\n" + std::string(20, 'z')), "\"abcdefghi\"...", 16); + Expect("too small output", Narrow("long enough text"), "", 7); + if (failures != 0) return 1; + std::puts("crash text: all cases passed"); + return 0; +} diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index 43dc8405c0..42079527da 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -14,6 +14,9 @@ #include #include #include +#ifdef __linux__ +#include +#endif #include extern "C" unsigned __cxa_uncaught_exceptions_nid_postfix(); @@ -172,6 +175,9 @@ void CheckDefaultTerminateMessage() { if (child == 0) { close(channel[0]); dup2(channel[1], 2); +#ifdef __linux__ + assert(prctl(PR_SET_DUMPABLE, 0) == 0); +#endif throw std::runtime_error(message); } close(channel[1]); diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 248325f569..a6407053b7 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -51,7 +51,7 @@ FiberStorage g_secondStorage; auto* const g_first = reinterpret_cast(&g_firstStorage); auto* const g_second = reinterpret_cast(&g_secondStorage); -void CheckActiveContext(FiberObject* fiber, std::uint64_t expectedSize) { +[[gnu::no_sanitize("address")]] void CheckActiveContext(FiberObject* fiber, std::uint64_t expectedSize) { FiberInfo info{}; info.size = sizeof(info); Require(sceFiberGetInfo(fiber, &info) == 0, "active fiber context info"); @@ -211,6 +211,26 @@ void RunFrameChain() { Require(sceFiberFinalize(fiber) == 0, "finalize chain"); } +constexpr std::size_t LargeContextSize = 512 * 1024; +alignas(16) std::array g_largeContext; +FiberStorage g_largeStorage; +bool g_largeOnContext = false; + +void APS5_VABI LargeContextEntry(std::uint64_t, std::uint64_t) { + volatile unsigned char local = 0; + const auto* address = const_cast(&local); + g_largeOnContext = address >= g_largeContext.data() && address < g_largeContext.data() + g_largeContext.size(); + sceFiberReturnToThread(0, nullptr); +} + +void RunOnLargeContext() { + auto* fiber = reinterpret_cast(&g_largeStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "large", LargeContextEntry, 0, g_largeContext.data(), g_largeContext.size(), nullptr, 0) == 0, "initialize large fiber"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run large fiber"); + Require(g_largeOnContext, "a fiber with a 512 KiB context runs on its context"); + Require(sceFiberFinalize(fiber) == 0, "finalize large fiber"); +} + [[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); @@ -259,4 +279,5 @@ int main() { RunHostCallOnSmallContext(); RunFrameChain(); + RunOnLargeContext(); } diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index ff80543263..0a4742b900 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -8,8 +9,11 @@ #include #include #include +#include #include #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include #include #else #include @@ -17,6 +21,7 @@ extern "C" { FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); +FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream); int APS5_VABI fclose_nid_postfix(FileStream* stream); std::size_t APS5_VABI fread_nid_postfix(void* buffer, std::size_t size, std::size_t count, FileStream* stream); @@ -31,8 +36,8 @@ int APS5_VABI ferror_nid_postfix(FileStream* stream); void APS5_VABI clearerr_nid_postfix(FileStream* stream); } -static void Require(bool condition) { - if (!condition) throw std::runtime_error("File stream check failed"); +static void Require(bool condition, const char* message = "File stream check failed") { + if (!condition) throw std::runtime_error(message); } template @@ -57,8 +62,62 @@ static void ReplaceWithReadOnlyDescriptor(FileStream* stream, const char* path) #endif } +static void CheckExclusiveCppOpen(const std::string& base) { + constexpr int Out = 0x02, Binary = 0x80, Noreplace = 0x40; + const std::string path = base + ".exclusive"; + std::remove(path.c_str()); + auto* stream = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0); + Require(stream != nullptr); + constexpr char original[] = "keep existing contents"; + Require(fwrite_nid_postfix(original, 1, sizeof(original), stream) == sizeof(original)); + Require(fclose_nid_postfix(stream) == 0); +#ifdef _WIN32 + const auto nativePath = std::filesystem::path(path).wstring(); + const auto writer = CreateFileW(nativePath.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(writer != INVALID_HANDLE_VALUE); + auto* refused = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0); + if (refused) fclose_nid_postfix(refused); + Require(CloseHandle(writer) != 0); + Require(refused == nullptr, "C++ noreplace truncated an existing write-shared file"); +#endif + stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::array saved{}; + Require(fread_nid_postfix(saved.data(), 1, saved.size(), stream) == saved.size()); + Require(std::memcmp(saved.data(), original, saved.size()) == 0); + Require(fclose_nid_postfix(stream) == 0 && std::remove(path.c_str()) == 0); + for (const int mode : {0x42, 0x52, 0x53, 0xc2, 0xd2, 0xd3}) { + stream = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), mode | 0x04, 0); + Require(stream != nullptr && ftell_nid_postfix(stream) == 0); + Require(fwrite_nid_postfix(original, 1, sizeof(original), stream) == sizeof(original)); + Require(fclose_nid_postfix(stream) == 0); + errno = 0; + Require(_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), mode, 0) == nullptr && errno == EEXIST); + stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr && fread_nid_postfix(saved.data(), 1, saved.size(), stream) == saved.size()); + Require(std::memcmp(saved.data(), original, saved.size()) == 0); + Require(fclose_nid_postfix(stream) == 0 && std::remove(path.c_str()) == 0); + } + for (int round = 0; round < 12; ++round) { + std::array created{}; + std::barrier start(2); + const auto create = [&](int index) { + start.arrive_and_wait(); + created[index] = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0); + }; + std::thread first(create, 0), second(create, 1); + first.join(); + second.join(); + const auto winners = static_cast(created[0] != nullptr) + static_cast(created[1] != nullptr); + for (auto* opened : created) if (opened) Require(fclose_nid_postfix(opened) == 0); + Require(winners == 1 && std::remove(path.c_str()) == 0); + } +} + int main(int argc, char** argv) { Require(argc == 2); + CheckExclusiveCppOpen(argv[1]); Require(_Stdout_nid_postfix.GetHandle() == stdout); Require(_Stderr_nid_postfix.GetHandle() == stderr); Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0); @@ -88,7 +147,7 @@ int main(int argc, char** argv) { ExpectException([] { fopen_nid_postfix(nullptr, "r"); }); FileStream closed(std::tmpfile()); closed.Close(); - ExpectException([&] { fflush_nid_postfix(&closed); }); + Require(fflush_nid_postfix(&closed) == -1 && errno == 9); Require(std::remove(argv[1]) == 0); errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 8b2260536d..e8693b984e 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -102,6 +102,8 @@ static bool CheckWidePrecision() { Require(snprintf_nid_postfix(output, sizeof(output), "[%5.1ls]", u"\u00e9") == 7 && std::strcmp(output, "[ ]") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "[%-5.2ls]", u"\u00e9") == 7 && std::strcmp(output, "[\xc3\xa9 ]") == 0); Require(FormatList(output, sizeof(output), "%.3ls", u"\U0001f600") == 0 && output[0] == 0); + Require(FormatList(output, sizeof(output), "%ls[% u][% 4u][%+ lu][%- 3hhu][% x][%+o]", u"", 5u, 5u, 5ULL, 5u, 255u, 8u) == 25 + && std::strcmp(output, "[5][ 5][5][5 ][ff][10]") == 0); Require(FormatList(output, sizeof(output), "%.*ls:%d", -1, u"\u00e9", 7) == 4 && std::strcmp(output, "\xc3\xa9:7") == 0); const char16_t bounded[] = {u'A', u'B'}; Require(snprintf_nid_postfix(output, sizeof(output), "%.2ls", bounded) == 2 && std::strcmp(output, "AB") == 0); diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index df3981cfb8..0406b512c3 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -7,6 +7,7 @@ #include extern "C" { +int APS5_VABI sceAgcDriverRegisterDefaultOwner(std::uint32_t); int APS5_VABI sceAgcDriverRegisterOwner(std::uint32_t*, const char*); int APS5_VABI sceAgcDriverRegisterResource(std::uint32_t*, std::uint32_t, const void*, std::size_t, const char*, std::uint32_t, std::uint64_t); int APS5_VABI sceAgcDriverRegisterMultipleResources(std::uint32_t*, std::uint32_t, const void*, std::uint32_t); @@ -47,6 +48,8 @@ int main() { std::uint32_t owner = 7u; std::uint32_t resource = 9u; const std::uint32_t memory[4]{}; + Require(sceAgcDriverRegisterDefaultOwner(0u) == 0); + Require(sceAgcDriverRegisterDefaultOwner(owner) == 0); Require(sceAgcDriverRegisterOwner(&owner, "owner") == Unavailable); Require(sceAgcDriverRegisterResource(&resource, owner, memory, sizeof(memory), "resource", 1u, 2u) == Unavailable); Require(sceAgcDriverRegisterGdsResource(&resource, owner, 0u, 64u, "gds", 1u, 2u) == Unavailable); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 5231993647..87d872eb74 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -1345,6 +1346,66 @@ std::uint32_t CreateOpus(std::uint32_t context) { return instance; } +void TestInstanceHandleLifetime(std::uint32_t context) { + const auto live = CreateOpus(context); + const auto reference = CreateOpus(context); + const std::size_t packetBytes = 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)); + std::vector pcm(128 * 2); + std::vector expected(pcm.size()); + DecodeSideband decoded{}; + DecodeSideband baseline{}; + RunDecode(context, live, OPUS_STEREO, packetBytes, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO, packetBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && decoded.outputWritten == 128 * 2 * 2 && decoded.totalDecodedSamples == 960); + Require(baseline.result == 0 && pcm == expected); + for (std::size_t index = 0; index < 0x8000; ++index) { + std::uint32_t temporary = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &temporary) == 0); + if (temporary == live || temporary == reference) throw std::runtime_error("AJM creation replaced an existing live decoder handle"); + Require(temporary >> 14 == 24); + Require(sceAjmInstanceDestroy(context, temporary) == 0); + } + std::vector occupied(0x4000); + occupied[live & 0x3FFF] = true; + occupied[reference & 0x3FFF] = true; + std::vector instances; + instances.reserve(0x4000 - 2); + for (std::size_t index = 0; index < 0x4000 - 2; ++index) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0 && instance >> 14 == 24); + Require(!occupied[instance & 0x3FFF]); + occupied[instance & 0x3FFF] = true; + instances.push_back(instance); + } + Require(std::all_of(occupied.begin(), occupied.end(), [](bool used) { return used; })); + constexpr int outOfResources = static_cast(0x80930007); + std::uint32_t refused = 0xDEADBEEF; + Require(sceAjmInstanceCreate(context, 24, 0, &refused) == outOfResources && refused == 0xDEADBEEF); + std::uint32_t otherCodec = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &otherCodec) == 0 && otherCodec >> 14 == 1); + const auto released = instances.back(); + Require(sceAjmInstanceDestroy(context, released) == 0); + Require(sceAjmInstanceCreate(context, 24, 0, &instances.back()) == 0 && instances.back() == released); + Require(sceAjmInstanceCreate(context, 24, 0, &refused) == outOfResources && refused == 0xDEADBEEF); + pcm.assign(832 * 2, -12345); + expected.assign(pcm.size(), -12345); + RunDecode(context, live, OPUS_STEREO, 0, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO, 0, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && decoded.inputConsumed == 0 && decoded.outputWritten == 832 * 2 * 2 && decoded.totalDecodedSamples == 960); + Require(baseline.result == 0 && baseline.outputWritten == decoded.outputWritten && pcm == expected); + const std::size_t nextPacketBytes = 2 + (OPUS_STEREO[packetBytes] | (std::size_t{OPUS_STEREO[packetBytes + 1]} << 8u)); + pcm.assign(960 * 2, -12345); + expected.assign(pcm.size(), -12345); + RunDecode(context, live, OPUS_STEREO + packetBytes, nextPacketBytes, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO + packetBytes, nextPacketBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == nextPacketBytes && decoded.outputWritten == 960 * 2 * 2 && decoded.totalDecodedSamples == 1920); + Require(baseline.result == 0 && baseline.outputWritten == decoded.outputWritten && pcm == expected); + for (const auto instance : instances) Require(sceAjmInstanceDestroy(context, instance) == 0); + Require(sceAjmInstanceDestroy(context, otherCodec) == 0); + Require(sceAjmInstanceDestroy(context, live) == 0); + Require(sceAjmInstanceDestroy(context, reference) == 0); +} + ResampleInfo GetResampleInfo(std::uint32_t context, std::uint32_t instance) { ResampleInfo resample; std::memset(&resample, 0xAA, sizeof(resample)); @@ -1636,6 +1697,77 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestOpusGaplessReset(std::uint32_t context, std::size_t outputFrames, float ratio) { + const auto instance = CreateOpus(context); + const auto reference = CreateOpus(context); + std::vector batch(4096); + std::vector pcm(outputFrames * 2 + 4, -12345); + std::vector expected(960 * 2); + const std::uint32_t totalSamples = ratio > 0 ? 860 : 500; + const GaplessDecode gapless{totalSamples, 100, 0}; + const GaplessDecode unlimited{0, 100, 0}; + AjmBatchInfo info{}; + if (ratio > 0) { + std::int32_t result[2][2] = {{-1, -1}, {-1, -1}}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetResampleParameters(&info, instance, ratio, 1, result[0]) == 0); + Require(sceAjmBatchJobSetResampleParameters(&info, reference, ratio, 1, result[1]) == 0); + Submit(context, info); + Require(result[0][0] == 0 && result[1][0] == 0); + } + std::size_t offset = 0; + for (int segment = 0; segment < 3; ++segment) { + std::int32_t result[2][2] = {{-1, -1}, {-1, -1}}; + GaplessSideband state{-1, -1, 0, 0, 0xffff}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 1, result[0]) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, reference, &unlimited, 1, result[1]) == 0); + Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &state) == 0); + Submit(context, info); + Require(result[0][0] == 0 && result[1][0] == 0); + Require(state.result == 0 && state.totalSamples == totalSamples && state.skipSamples == 100 && state.skippedSamples == 0); + const std::size_t packetBytes = 2 + (OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u)); + DecodeSideband baseline{}; + RunDecode(context, reference, OPUS_STEREO + offset, packetBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(baseline.result == 0 && static_cast(baseline.inputConsumed) == packetBytes); + Require(baseline.totalDecodedSamples == static_cast(segment + 1) * 860 + (ratio == 0 ? segment * 960 : 0)); + const std::size_t expectedBytes = ratio > 0 ? static_cast(baseline.outputWritten) : totalSamples * 2 * sizeof(std::int16_t); + Require(expectedBytes > 0); + std::size_t produced = 0; + for (int drain = 0; produced < expectedBytes && drain < 16; ++drain) { + std::fill(pcm.begin(), pcm.end(), -12345); + DecodeSideband decoded{}; + RunDecode(context, instance, OPUS_STEREO + offset, drain == 0 ? packetBytes : 0, pcm.data(), outputFrames * 2 * sizeof(std::int16_t), decoded); + if (decoded.outputWritten <= 0) throw std::runtime_error("Opus gapless segment " + std::to_string(segment) + " wrote " + std::to_string(decoded.outputWritten) + " bytes with " + std::to_string(expectedBytes - produced) + " still expected"); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == (drain == 0 ? packetBytes : 0)); + Require(decoded.totalDecodedSamples == static_cast(segment + 1) * totalSamples); + const auto samples = static_cast(decoded.outputWritten) / sizeof(std::int16_t); + Require(samples <= outputFrames * 2 && produced + static_cast(decoded.outputWritten) <= expectedBytes); + Require(std::equal(pcm.begin(), pcm.begin() + samples, expected.begin() + produced / sizeof(std::int16_t))); + Require(std::all_of(pcm.begin() + samples, pcm.end(), [](auto sample) { return sample == -12345; })); + produced += static_cast(decoded.outputWritten); + } + Require(produced == expectedBytes); + if (ratio == 0) { + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 0, result[0]) == 0); + Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &state) == 0); + Submit(context, info); + Require(result[0][0] == 0 && state.result == 0 && state.skippedSamples == 100); + std::fill(pcm.begin(), pcm.end(), -12345); + DecodeSideband decoded{}; + RunDecode(context, instance, OPUS_STEREO + offset, packetBytes, pcm.data(), outputFrames * 2 * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO + offset, packetBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == packetBytes && decoded.outputWritten == 0); + Require(decoded.totalDecodedSamples == static_cast(segment + 1) * totalSamples); + Require(std::all_of(pcm.begin(), pcm.end(), [](auto sample) { return sample == -12345; })); + } + offset = (offset + packetBytes) % sizeof(OPUS_STEREO); + } + Require(sceAjmInstanceDestroy(context, instance) == 0); + Require(sceAjmInstanceDestroy(context, reference) == 0); +} + } int main() { @@ -1688,11 +1820,15 @@ int main() { TestCodecInfo(context); TestGetInfo(context); TestBatchWaitRelease(context); + TestInstanceHandleLifetime(context); TestBatchCancel(context); TestControlAt9(context); TestMultichannelAt9(context); TestControlMp3(context); TestControlOpus(context); + TestOpusGaplessReset(context, 960, 0); + TestOpusGaplessReset(context, 128, 0); + TestOpusGaplessReset(context, 960, 2.0f); std::vector delayedAac; TestM4aacAdts(context, delayedAac); TestM4aacNondelay(context, delayedAac); diff --git a/core/libs/tests/GuestAllocatingStrings.cpp b/core/libs/tests/GuestAllocatingStrings.cpp new file mode 100644 index 0000000000..8bf053914b --- /dev/null +++ b/core/libs/tests/GuestAllocatingStrings.cpp @@ -0,0 +1,138 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); +char* APS5_VABI strdup_nid_postfix(const char*); +int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); +int APS5_VABI vasprintf_nid_postfix(char**, const char*, VaList*); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +bool failAllocation = false; + +void* APS5_VABI Allocate(std::size_t size) { return failAllocation ? nullptr : std::malloc(size); } +void APS5_VABI Free(void* ptr) { std::free(ptr); } +void* APS5_VABI Reallocate(void* ptr, std::size_t size) { return std::realloc(ptr, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* ptr, std::size_t size, std::size_t) { return std::realloc(ptr, size); } +int APS5_VABI PosixAlign(void** ptr, std::size_t, std::size_t size) { + *ptr = std::malloc(size); + return *ptr ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array process{}; + alignas(16) static std::array libc{}; + alignas(16) static std::array replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast(&Allocate)); + write(replacement, 0x28, reinterpret_cast(&Free)); + write(replacement, 0x30, reinterpret_cast(&Calloc)); + write(replacement, 0x38, reinterpret_cast(&Reallocate)); + write(replacement, 0x40, reinterpret_cast(&Align)); + write(replacement, 0x48, reinterpret_cast(&Realign)); + write(replacement, 0x50, reinterpret_cast(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + +int CallVasprintf(char** destination, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vasprintf_nid_postfix(destination, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + +} + +int main() { + *__error_nid_postfix() = 0; + Require(strndup_nid_postfix(nullptr, 0) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(strndup_nid_postfix(nullptr, 32) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(strdup_nid_postfix(nullptr) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(vasprintf_nid_postfix(nullptr, "format", nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + char* allocated = nullptr; + *__error_nid_postfix() = 0; + Require(CallVasprintf(&allocated, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + Require(allocated == nullptr); + + *__error_nid_postfix() = 0; + Require(vasprintf_nid_postfix(&allocated, "format", nullptr) == -1); + Require(*__error_nid_postfix() == 22); + Require(allocated == nullptr); + + InitializeHeap(); + + char* dup1 = strndup_nid_postfix("hello world", 5); + Require(dup1 != nullptr); + Require(std::strcmp(dup1, "hello") == 0); + free_nid_postfix(dup1); + + char* dup2 = strdup_nid_postfix("testing 1 2 3"); + Require(dup2 != nullptr); + Require(std::strcmp(dup2, "testing 1 2 3") == 0); + free_nid_postfix(dup2); + + allocated = nullptr; + const int count = asprintf_nid_postfix(&allocated, "value=%d %s", 42, "ok"); + Require(count == 11); + Require(allocated != nullptr); + Require(std::strcmp(allocated, "value=42 ok") == 0); + free_nid_postfix(allocated); + + failAllocation = true; + allocated = nullptr; + *__error_nid_postfix() = 0; + Require(asprintf_nid_postfix(&allocated, "value=%d", 1) == -1); + Require(*__error_nid_postfix() == 12); + Require(allocated == nullptr); + failAllocation = false; + + return 0; +} diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 22629e0166..5312249a50 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -32,6 +32,17 @@ constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits int failures = 0; +#ifdef __linux__ +int overcommitMode() { + int mode = -1; + if (std::FILE* file = std::fopen("/proc/sys/vm/overcommit_memory", "r")) { + if (std::fscanf(file, "%d", &mode) != 1) mode = -1; + std::fclose(file); + } + return mode; +} +#endif + void report(const char* what, const char* detail) { std::fprintf(stderr, "%s: %s\n", what, detail); ++failures; @@ -107,6 +118,9 @@ int main() { free_nid_postfix(block); } +#ifdef ANYPS5_ENABLE_ASAN + std::fputs("skipped: AddressSanitizer cannot map the shadow of a 300 GiB commit under a 128 MiB RLIMIT_DATA, so the failed commit and arena release checks did not run\n", stderr); +#else if (GuestArena::GuestArenaAvailable_nid_postfix()) { #ifdef __linux__ rlimit originalLimit{}; @@ -151,7 +165,14 @@ int main() { #endif if (committed) { #ifdef __linux__ - report("malloc(300 GiB)", "succeeded despite RLIMIT_DATA"); + int mode = overcommitMode(); + if (mode == 1) { + std::fprintf(stderr, "skipped: the host can commit 300 GiB (vm.overcommit_memory=%d), so the failed commit and arena release checks did not run\n", mode); + } else { + char detail[64]; + std::snprintf(detail, sizeof(detail), "committed 300 GiB with vm.overcommit_memory=%d", mode); + report("malloc(300 GiB)", detail); + } #else std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); #endif @@ -175,6 +196,7 @@ int main() { if (large == nullptr) report("malloc(1 MiB) after a failed huge malloc", "returned NULL"); free_nid_postfix(large); } +#endif void* untouched = &failures; *__error_nid_postfix() = 77; diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index ef98b12fed..264ff62a6d 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -76,6 +77,9 @@ 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(); +LibcException::Header* APS5_VABI __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)); +[[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException); +extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; } static constexpr int ThrdSuccess = 0; @@ -487,11 +491,11 @@ static void APS5_VABI ExitWithSystemErrorCode() { } template -static int GuestSystemErrorCode(TCall call) { +static int TerminateExitCode(GuestTerminate handler, TCall call) { const pid_t child = fork(); Require(child >= 0); if (child == 0) { - _ZSt13set_terminatePFvvE_nid_postfix(ExitWithSystemErrorCode); + _ZSt13set_terminatePFvvE_nid_postfix(handler); call(); _exit(2); } @@ -499,6 +503,23 @@ static int GuestSystemErrorCode(TCall call) { Require(waitpid(child, &status, 0) == child); return WIFEXITED(status) ? WEXITSTATUS(status) : -1; } + +static const GuestTypeInfo UnexpectedType{_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 16, "10Unexpected"}; + +static void APS5_VABI ExitWithCaughtType() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + _exit(type == &UnexpectedType ? 42 : type == nullptr ? 43 : 1); +} + +static void TestCallUnexpected() { + Require(TerminateExitCode(ExitWithCaughtType, [] { + void* object = __cxa_allocate_exception_nid_postfix(sizeof(int)); + auto* type = reinterpret_cast(const_cast(&UnexpectedType)); + LibcException::Header* header = __cxa_init_primary_exception_nid_postfix(object, type, nullptr); + __cxa_call_unexpected_nid_postfix(&header->unwind); + }) == 42); + Require(TerminateExitCode(ExitWithCaughtType, [] { __cxa_call_unexpected_nid_postfix(nullptr); }) == 43); +} #endif static void TestPadErrors() { @@ -515,7 +536,7 @@ static void TestPadErrors() { 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); + Require(TerminateExitCode(ExitWithSystemErrorCode, [&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); #endif Require(!pad.started); @@ -538,5 +559,8 @@ int main() { TestSpinLock(); TestWcstombs(); TestConditionSignal(); +#ifndef _WIN32 + TestCallUnexpected(); +#endif return 0; } diff --git a/core/libs/tests/GuestAppArchive.cpp b/core/libs/tests/GuestAppArchive.cpp new file mode 100644 index 0000000000..1f865808a5 --- /dev/null +++ b/core/libs/tests/GuestAppArchive.cpp @@ -0,0 +1,178 @@ +#include "AppArchiveFixture.hpp" +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::cerr << message << '\n'; + std::exit(1); + } +} + +bool Lookup(const char* guestPath, AppArchiveNode& node) { + return AppArchiveLookup_nid_no_patch(ResolveArchivedPath_nid_no_patch(guestPath), &node); +} + +bool GuardRefuses(const char* guestPath) { + try { + (void)ResolvePath_nid_no_patch(guestPath); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), "not implemented") != nullptr && std::strstr(error.what(), guestPath) != nullptr; + } + return false; +} + +constexpr std::size_t BigSize = 200 * 1024 + 123; + +std::map> Contents() { + return { + {"Data/Big.bin", AppArchiveFixture::Pattern(BigSize)}, + {"Data/Empty.txt", {}}, + {"Data/Sub/Deep.txt", {'d', 'e', 'e', 'p'}}, + {"Readme.TXT", {'r', 'e', 'a', 'd'}}, + }; +} + +void Present(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", Contents(), {"Data/Unused"}); + { std::ofstream eboot(root / "app0" / "eboot.bin"); eboot << "elf"; } + Require(AppArchivePath_nid_no_patch() == root / "app0.zar", "the archive beside app0 was not opened"); + + AppArchiveNode node{}; + Require(Lookup("/app0/data/BIG.BIN", node) && !node.Directory && node.Size == BigSize, "a file was not found case-insensitively with its size"); + const auto big = node.Handle; + Require(Lookup("/APP0/Data/Empty.txt", node) && !node.Directory && node.Size == 0, "an empty file was not found"); + Require(Lookup("/app0/Data", node) && node.Directory, "a directory was not found"); + const auto data = node.Handle; + Require(Lookup("/app0/", node) && node.Directory, "the archive root was not app0"); + Require(Lookup("/app0/Data/Sub/../Sub/deep.txt", node) && node.Size == 4, "a normalised path was not found"); + Require(!Lookup("/app0/Data/Missing.bin", node), "a missing file was found"); + Require(!Lookup("/app0/Readme.TXT/x", node), "a path through a file was found"); + Require(!Lookup("/app0x/Readme.TXT", node), "a sibling of app0 was looked up in the archive"); + Require(!Lookup("/Readme.TXT", node), "a path outside app0 was looked up in the archive"); + + Require(GuardRefuses("/app0/Data/Big.bin"), "an archive-only file resolved through an unaware call"); + Require(GuardRefuses("/app0/Data"), "an archive-only directory resolved through an unaware call"); + Require(ResolvePath_nid_no_patch("/app0/eboot.bin") == root / "app0" / "eboot.bin", "a file on disk was refused"); + Require(ResolvePath_nid_no_patch("/app0/Data/Missing.bin") == root / "app0" / "Data" / "Missing.bin", "a path missing everywhere was refused"); + Require(Lookup("/app0/sce_sys", node) == false && ResolvePath_nid_no_patch("/app0/sce_sys") == root / "app0" / "sce_sys", "a directory only on disk was not left to the disk"); + + bool rejected = false; + const auto expected = AppArchiveFixture::Pattern(BigSize); + std::vector buffer(BigSize + 64, 0); + Require(AppArchiveRead_nid_no_patch(big, 0, buffer.data(), BigSize) == BigSize && std::equal(expected.begin(), expected.end(), buffer.begin()), "the whole file did not read back"); + for (const std::uint64_t offset : {std::uint64_t{1}, std::uint64_t{65535}, std::uint64_t{65536}, std::uint64_t{131071}, std::uint64_t{BigSize - 5}}) { + std::fill(buffer.begin(), buffer.end(), 0); + const auto read = AppArchiveRead_nid_no_patch(big, offset, buffer.data(), 70000); + const auto wanted = std::min(70000, BigSize - offset); + Require(read == wanted && std::equal(expected.begin() + static_cast(offset), expected.begin() + static_cast(offset + wanted), buffer.begin()), "a read across block boundaries returned wrong bytes"); + } + Require(AppArchiveRead_nid_no_patch(big, BigSize, buffer.data(), 10) == 0, "a read at the end returned bytes"); + Require(AppArchiveRead_nid_no_patch(big, 10, buffer.data(), 0) == 0, "a zero-length read returned bytes"); + + AppArchiveNode bigNode{}; + Require(Lookup("/app0/Data/Big.bin", bigNode), "the big file vanished"); + constexpr int Descriptor = 1000003; + constexpr int Shared = 1000004; + Require(!AppArchiveDescriptor_nid_no_patch(Descriptor, nullptr, nullptr), "a descriptor was attached before attaching"); + AppArchiveAttach_nid_no_patch(Descriptor, bigNode); + AppArchiveShare_nid_no_patch(Descriptor, Shared); + std::uint64_t done = 0; + std::vector first(65530), second(20); + const AppArchiveBuffer split[] = {{first.data(), first.size()}, {second.data(), second.size()}}; + Require(AppArchiveDescriptorRead_nid_no_patch(Descriptor, split, 2, nullptr, &done) && done == first.size() + second.size(), "a vector read returned the wrong length"); + Require(std::equal(first.begin(), first.end(), expected.begin()) && std::equal(second.begin(), second.end(), expected.begin() + 65530), "a vector read returned wrong bytes"); + std::uint64_t position = 0; + Require(AppArchiveDescriptor_nid_no_patch(Shared, nullptr, &position) && position == 65550, "a shared descriptor did not share the position"); + const std::int64_t at = 131000; + const AppArchiveBuffer single[] = {{second.data(), second.size()}}; + Require(AppArchiveDescriptorRead_nid_no_patch(Shared, single, 1, &at, &done) && done == 20 && std::equal(second.begin(), second.end(), expected.begin() + at), "a positioned read returned wrong bytes"); + Require(AppArchiveDescriptor_nid_no_patch(Descriptor, nullptr, &position) && position == 65550, "a positioned read moved the position"); + std::int64_t sought = 0; + Require(AppArchiveDescriptorSeek_nid_no_patch(Shared, -3, 2, &sought) && sought == static_cast(BigSize - 3), "seeking from the end failed"); + Require(AppArchiveDescriptorRead_nid_no_patch(Descriptor, single, 1, nullptr, &done) && done == 3, "a read at the end was not short"); + Require(AppArchiveDescriptorRead_nid_no_patch(Descriptor, single, 1, nullptr, &done) && done == 0, "a read past the end returned bytes"); + Require(AppArchiveDescriptorSeek_nid_no_patch(Descriptor, -1, 0, &sought) && sought == -1, "a negative seek was accepted"); + Require(AppArchiveDescriptorSeek_nid_no_patch(Descriptor, 10, 1, &sought) && sought == static_cast(BigSize + 10), "seeking past the end failed"); + AppArchiveDetach_nid_no_patch(Descriptor); + Require(!AppArchiveDescriptor_nid_no_patch(Descriptor, nullptr, nullptr) && AppArchiveDescriptor_nid_no_patch(Shared, nullptr, nullptr), "detaching one descriptor detached its duplicate"); + AppArchiveShare_nid_no_patch(Descriptor, Shared); + Require(!AppArchiveDescriptor_nid_no_patch(Shared, nullptr, nullptr), "duplicating a plain descriptor over an archived one kept the archive"); + rejected = false; + try { AppArchiveAttach_nid_no_patch(Descriptor, AppArchiveNode{0, true, 0}); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected, "a directory was attached to a descriptor"); + + std::set> names; + for (const auto& entry : AppArchiveList_nid_no_patch(data)) names.insert({entry.Name, entry.Directory}); + Require(names == std::set>{{"Big.bin", false}, {"Empty.txt", false}, {"Sub", true}, {"Unused", true}}, "the directory listing is wrong"); + + rejected = false; + try { (void)AppArchiveList_nid_no_patch(big); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected, "a file was listed as a directory"); + rejected = false; + try { (void)AppArchiveRead_nid_no_patch(data, 0, buffer.data(), 1); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected, "a directory was read as a file"); +} + +void Absent() { + AppArchiveNode node{}; + Require(AppArchivePath_nid_no_patch().empty(), "an archive was reported without app0.zar"); + Require(!Lookup("/app0/Readme.TXT", node), "a path was found without an archive"); + Require(ResolvePath_nid_no_patch("/app0/Readme.TXT").filename() == "Readme.TXT", "resolution changed without an archive"); +} + +void Damaged(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", Contents()); + { + std::fstream archive(root / "app0.zar", std::ios::binary | std::ios::in | std::ios::out); + const char garbage[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00}; + archive.seekp(0); + archive.write(garbage, sizeof(garbage)); + } + AppArchiveNode node{}; + Require(Lookup("/app0/Data/Big.bin", node), "the damaged archive's index was not read"); + std::vector buffer(16); + bool thrown = false; + try { (void)AppArchiveRead_nid_no_patch(node.Handle, 0, buffer.data(), buffer.size()); } catch (const std::runtime_error& error) { thrown = std::strstr(error.what(), "damaged") != nullptr; } + Require(thrown, "a damaged block did not fail the read"); +} + +void Invalid(const std::filesystem::path& root) { + { std::ofstream archive(root / "app0.zar", std::ios::binary); archive << "not an archive"; } + AppArchiveNode node{}; + bool thrown = false; + try { (void)Lookup("/app0/Readme.TXT", node); } catch (const std::runtime_error& error) { thrown = std::strstr(error.what(), "app0.zar") != nullptr; } + Require(thrown, "an invalid app0.zar was not reported"); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: guest_app_archive_tests present|absent|damaged|invalid\n"; + return 2; + } + const std::string mode = argv[1]; + const auto root = AppArchiveFixture::EnterFreshRoot("anyps5-app-archive"); + if (mode == "present") Present(root); + else if (mode == "absent") Absent(); + else if (mode == "damaged") Damaged(root); + else if (mode == "invalid") Invalid(root); + else return 2; + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 8fb8a16677..fef723221e 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabe int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t, const NpUnifiedEntitlementLabel*, void*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -88,6 +89,15 @@ int main() { Require(sceAppContentAddcontEnqueueDownload(7, &label) == ErrorNotFound); Require(sceAppContentAddcontDelete(0, &label) == ErrorNotFound); Require(sceAppContentAddcontDelete(7, nullptr) == ErrorParameter); + unsigned char progress[64]; + std::memset(progress, 0x5a, sizeof(progress)); + unsigned char progressBefore[sizeof(progress)]; + std::memcpy(progressBefore, progress, sizeof(progress)); + Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == ErrorNotFound); + Require(sceAppContentGetAddcontDownloadProgress(7, &label, progress) == ErrorNotFound); + Require(sceAppContentGetAddcontDownloadProgress(0, nullptr, progress) == ErrorParameter); + Require(sceAppContentGetAddcontDownloadProgress(0, &label, nullptr) == ErrorParameter); + Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == ErrorParameter); Require(sceAppContentAddcontMount(0, &label, &mountPoint) == ErrorNotFound); diff --git a/core/libs/tests/GuestAppIcon.cpp b/core/libs/tests/GuestAppIcon.cpp new file mode 100644 index 0000000000..0657ac60f2 --- /dev/null +++ b/core/libs/tests/GuestAppIcon.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +int CheckNoIcon(const char* root) { + std::filesystem::current_path(root); + Require(!HasAppIcon_nid_postfix()); + bool threw = false; + try { + GetAppIconData_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon not available") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckValidIcon(const char* root) { + std::filesystem::current_path(root); + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.bytes != nullptr); + Require(icon.size == 16); + Require(icon.bytes[0] == 0x89 && icon.bytes[1] == 'P' && icon.bytes[2] == 'N' && icon.bytes[3] == 'G'); + return 0; +} + +int CheckEmptyIcon(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is empty or unreadable") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckCorruptPng(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is not a valid PNG file") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckConcurrentAccess(const char* root) { + std::filesystem::current_path(root); + std::vector threads; + for (int i = 0; i < 8; ++i) { + threads.emplace_back([] { + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.size == 16); + }); + } + for (auto& t : threads) t.join(); + return 0; +} + +void RunCase(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_app_icon_" + 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 stream(root / "app0" / "sce_sys" / name, std::ios::binary); + stream.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); +} + +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-icon") return CheckNoIcon(argv[2]); + if (mode == "--valid-icon") return CheckValidIcon(argv[2]); + if (mode == "--empty-icon") return CheckEmptyIcon(argv[2]); + if (mode == "--corrupt-png") return CheckCorruptPng(argv[2]); + if (mode == "--concurrent") return CheckConcurrentAccess(argv[2]); + return 1; + } + + const std::string validPng("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00", 16); + const std::string corruptPng("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", 16); + const std::string emptyPng(""); + + RunCase(argv[0], "--no-icon", {}); + RunCase(argv[0], "--valid-icon", {{"icon0.png", validPng}}); + RunCase(argv[0], "--empty-icon", {{"icon0.png", emptyPng}}); + RunCase(argv[0], "--corrupt-png", {{"icon0.png", corruptPng}}); + RunCase(argv[0], "--concurrent", {{"icon0.png", validPng}}); + + return 0; +} diff --git a/core/libs/tests/GuestAppendWrites.cpp b/core/libs/tests/GuestAppendWrites.cpp new file mode 100644 index 0000000000..0780e303eb --- /dev/null +++ b/core/libs/tests/GuestAppendWrites.cpp @@ -0,0 +1,110 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { + const void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +int APS5_VABI dup_nid_postfix(int); +} + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "append check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr const char* Path = "guest_append_writes.txt"; +static constexpr int CreateWriteAppend = 0x0209; +static constexpr int Truncate = 0x0400; +static constexpr int FromStart = 0; +static constexpr int FromCurrent = 1; +static constexpr int Writers = 4; +static constexpr int LinesPerWriter = 1500; +static constexpr std::size_t LineLength = 32; + +static std::string Line(int writer, int index) { + std::string line = "w" + std::to_string(writer) + " " + std::to_string(index) + " "; + line.resize(LineLength - 1, '.'); + return line + "\n"; +} + +static void WriteLines(int fd, int writer) { + for (int index = 0; index < LinesPerWriter; ++index) { + const std::string line = Line(writer, index); + std::int64_t written = -1; + if (writer == 0 || writer == 2) { + written = sceKernelWrite(fd, line.data(), line.size()); + } else if (writer == 1) { + written = write_nid_postfix(fd, line.data(), static_cast(line.size())); + } else { + const GuestIovec parts[2]{{line.data(), 7}, {line.data() + 7, line.size() - 7}}; + written = writev_nid_postfix(fd, parts, 2); + } + Require(written == static_cast(LineLength)); + } +} + +static std::string Contents() { + std::ifstream file(Path, std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +int main() { + const int first = sceKernelOpen(Path, CreateWriteAppend | Truncate, 0644); + Require(first >= 0); + const int second = sceKernelOpen(Path, CreateWriteAppend, 0644); + const int duplicate = dup_nid_postfix(first); + const int vectored = sceKernelOpen(Path, CreateWriteAppend, 0644); + Require(second >= 0 && duplicate >= 0 && vectored >= 0); + + const int descriptors[Writers]{first, second, duplicate, vectored}; + std::vector threads; + for (int writer = 0; writer < Writers; ++writer) threads.emplace_back(WriteLines, descriptors[writer], writer); + for (auto& thread : threads) thread.join(); + + const std::string contents = Contents(); + Require(contents.size() == static_cast(Writers) * LinesPerWriter * LineLength); + std::set> seen; + for (std::size_t offset = 0; offset < contents.size(); offset += LineLength) { + const std::string line = contents.substr(offset, LineLength); + int writer = -1; + int index = -1; + Require(std::sscanf(line.c_str(), "w%d %d ", &writer, &index) == 2); + Require(writer >= 0 && writer < Writers && index >= 0 && index < LinesPerWriter); + Require(line == Line(writer, index)); + Require(seen.emplace(writer, index).second); + } + Require(seen.size() == static_cast(Writers) * LinesPerWriter); + + const auto size = static_cast(contents.size()); + Require(sceKernelLseek(first, 0, FromStart) == 0); + const std::string tail = Line(9, 0); + Require(sceKernelWrite(first, tail.data(), tail.size()) == static_cast(LineLength)); + Require(sceKernelLseek(first, 0, FromCurrent) == size + static_cast(LineLength)); + Require(Contents().substr(static_cast(size)) == tail); + + for (const int fd : descriptors) Require(sceKernelClose(fd) == 0); + std::remove(Path); +} diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index fa5d8f2431..6c754ac77a 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -1,9 +1,11 @@ #include "SceTypes.hpp" +#include #include #include #include #include #include +#include #include extern "C" { @@ -13,6 +15,7 @@ int APS5_VABI sceAudio3dTerminate(); int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, std::uint32_t* id); int APS5_VABI sceAudio3dPortClose(std::uint32_t port_id); int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t attribute_id, const void* attribute, std::size_t attribute_size); +int APS5_VABI sceAudio3dPortGetAttributesSupported(std::uint32_t port_id, std::uint32_t* attributes, std::uint32_t* num_attributes); int APS5_VABI sceAudio3dPortGetQueueLevel(std::uint32_t port_id, std::uint32_t* queue_level, std::uint32_t* queue_available); int APS5_VABI sceAudio3dPortAdvance(std::uint32_t port_id); int APS5_VABI sceAudio3dPortPush(std::uint32_t port_id, std::uint32_t blocking); @@ -76,6 +79,9 @@ void CheckBeforeInitialize() { Require(sceAudio3dPortOpen(1, nullptr, nullptr) == NOT_READY, "NOT_READY is checked before the arguments"); std::uint32_t level = 0; Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == INVALID_PORT, "queue level needs an open port"); + std::uint32_t num_attributes = 3; + std::uint32_t attributes[3]{}; + Require(sceAudio3dPortGetAttributesSupported(0, attributes, &num_attributes) == INVALID_PORT, "supported attributes need an open port"); Require(sceAudio3dPortClose(0) == INVALID_PORT, "close needs an open port"); Require(sceAudio3dPortAdvance(0) == INVALID_PORT, "advance needs an open port"); Require(sceAudio3dPortPush(0, 0) == INVALID_PORT, "push needs an open port"); @@ -137,6 +143,19 @@ void CheckOpenClose() { void CheckAttributes() { float value = 0.5f; int flag = 1; + constexpr std::uint32_t supported[]{0x10001, 0x10002, 0x10003}; + std::uint32_t num_attributes = 99; + Require(sceAudio3dPortGetAttributesSupported(0, nullptr, &num_attributes) == 0 && num_attributes == 3, "null output reports the supported attribute count"); + Require(sceAudio3dPortGetAttributesSupported(1, nullptr, &num_attributes) == INVALID_PORT, "supported attributes reject an invalid port"); + Require(sceAudio3dPortGetAttributesSupported(0, nullptr, nullptr) == INVALID_PARAMETER, "supported attributes require a count pointer"); + num_attributes = 2; + std::uint32_t partial[3]{0, 0, 99}; + Require(sceAudio3dPortGetAttributesSupported(0, partial, &num_attributes) == 0 && num_attributes == 2, "supported attributes respect the caller capacity"); + Require(partial[0] == supported[0] && partial[1] == supported[1] && partial[2] == 99, "supported attributes copy only the requested entries"); + num_attributes = 3; + std::uint32_t all[3]{}; + Require(sceAudio3dPortGetAttributesSupported(0, all, &num_attributes) == 0 && num_attributes == 3, "all supported attributes are returned"); + Require(std::equal(std::begin(all), std::end(all), std::begin(supported)), "the reported attributes match the port setter"); Require(sceAudio3dPortSetAttribute(0, 0x10001, nullptr, 4) == INVALID_PARAMETER, "null attribute"); Require(sceAudio3dPortSetAttribute(1, 0x10001, &value, 4) == INVALID_PORT, "attribute on port 1"); Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, 4) == 0, "late reverb level"); diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 2e6af3008a..5e9ee471f1 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include extern "C" { @@ -143,6 +144,60 @@ const std::uint8_t MP3_MONO[] = { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, }; +const std::uint8_t HEAAC_V2_MONO[] = { + 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x22, 0x80, 0xA3, 0x6E, 0xEF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4A, 0x80, + 0x00, 0x06, 0xF5, 0x61, 0x0A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x42, 0x80, 0xA3, 0x78, 0x46, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x76, 0xE4, 0x89, 0xB5, 0x65, 0xE5, 0xDF, 0xFE, 0xB7, 0x32, 0x7C, 0x7F, 0xF0, 0x00, 0x06, 0xF4, 0xC1, 0x0A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x18, 0xA2, 0x38, 0x01, + 0x0A, 0x97, 0x98, 0xD8, 0x73, 0x75, 0x18, 0xBB, 0x6D, 0xCD, 0xA6, 0xE8, 0x7F, 0x09, 0xD5, 0xA0, 0x7E, 0x99, 0xC7, 0x42, 0xF2, 0x57, 0xBB, 0xC8, + 0xAB, 0x51, 0x57, 0x0A, 0xBF, 0x56, 0x8A, 0x40, 0x48, 0xFB, 0xCC, 0xF1, 0xFF, 0x4E, 0x4C, 0x21, 0xEE, 0x7F, 0x10, 0xFD, 0xA6, 0xB4, 0xE2, 0xCC, + 0x52, 0xA9, 0x38, 0x00, 0x00, 0xC4, 0x1A, 0x9A, 0xD2, 0x05, 0xE8, 0x94, 0xDB, 0xA5, 0x47, 0xFE, 0x92, 0xFC, 0x12, 0x41, 0x11, 0x08, 0x01, 0x22, + 0x65, 0x54, 0x28, 0x24, 0x16, 0x72, 0xE9, 0xAA, 0xC7, 0x45, 0x33, 0xEC, 0x74, 0x13, 0xAC, 0x84, 0xD5, 0xD4, 0x95, 0xCB, 0x41, 0x9B, 0x55, 0x2D, + 0xD5, 0xAC, 0x5C, 0xEF, 0xD4, 0x75, 0xF8, 0xD5, 0xCE, 0xC9, 0x64, 0xD9, 0xF5, 0x80, 0xD3, 0xD9, 0xAA, 0x5F, 0x1E, 0x45, 0x4F, 0xEA, 0x5F, 0xF8, + 0xBF, 0x48, 0x1F, 0xAD, 0xDF, 0x0C, 0xE5, 0xF1, 0x7F, 0x65, 0xC0, 0x12, 0xFA, 0xBF, 0xE6, 0x40, 0x0D, 0xAA, 0xFD, 0x40, 0x02, 0x58, 0x64, 0xA4, + 0x03, 0x2A, 0xC1, 0xAA, 0x79, 0x01, 0x6D, 0x37, 0x60, 0x78, 0x7B, 0x2B, 0x1B, 0x62, 0x6C, 0x87, 0xA7, 0xA2, 0xDB, 0x65, 0x00, 0xDE, 0x0F, 0xA0, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x01, 0xDB, 0x92, 0x26, 0xD5, 0x97, 0x97, 0x7F, 0xFA, 0xDC, 0xC9, 0xF1, 0xFF, 0xC0, 0x01, 0xC0, 0xFF, 0xF1, 0x58, + 0x40, 0x15, 0x02, 0x10, 0x01, 0x3C, 0xF3, 0x08, 0xB6, 0xA1, 0x99, 0x0C, 0x42, 0x0A, 0xA1, 0x40, 0xF6, 0x05, 0x0A, 0x6C, 0x0B, 0xC0, 0x1F, 0x1F, + 0x19, 0x9D, 0x15, 0x2A, 0x56, 0x97, 0x6C, 0x63, 0x88, 0xF4, 0x89, 0x13, 0x53, 0xA1, 0x9D, 0x8E, 0x9A, 0x93, 0x0C, 0x6A, 0x18, 0x0F, 0x2C, 0xCB, + 0x53, 0xB3, 0x93, 0xA9, 0xA9, 0x32, 0x63, 0xC3, 0x0C, 0x07, 0xF0, 0xF1, 0x3C, 0x98, 0xB9, 0x6B, 0x75, 0xB0, 0x95, 0xAD, 0x62, 0xDC, 0xA4, 0xB4, + 0x75, 0xE1, 0xAD, 0xE9, 0x64, 0x31, 0x51, 0x24, 0xAE, 0x6E, 0x20, 0xC0, 0x9D, 0x70, 0x25, 0x47, 0x1D, 0x67, 0x4D, 0x5C, 0xE1, 0xDA, 0x4E, 0x8A, + 0x9E, 0xF9, 0xFA, 0x62, 0x16, 0xDA, 0x99, 0x9F, 0xD0, 0xDC, 0x7D, 0xBD, 0x5F, 0x31, 0x8B, 0xC2, 0xE8, 0xD0, 0xB8, 0xE5, 0xC6, 0xC0, 0x37, 0x8F, + 0x68, 0xBC, 0x92, 0x4C, 0xD0, 0x00, 0x01, 0xFC, 0xFD, 0x43, 0xF2, 0xF5, 0xFF, 0x9F, 0xBF, 0xAD, 0x6D, 0x56, 0x0D, 0x3F, 0xF4, 0xA5, 0xD9, 0x17, + 0xFF, 0x20, 0x02, 0x7C, 0x02, 0xCC, 0x8B, 0x26, 0x49, 0xFF, 0xFB, 0x9E, 0xA2, 0x00, 0x0F, 0xE4, 0xDF, 0xF0, 0x00, 0x00, 0x70, +}; + +const std::uint8_t HEAAC_STEREO[] = { + 0xFF, 0xF1, 0x58, 0x80, 0x10, 0x02, 0xC0, 0x21, 0x21, 0x45, 0x00, 0x14, 0x50, 0x01, 0x46, 0xAD, 0xD5, 0x81, 0x75, 0x00, 0x00, 0x00, 0x1C, 0x60, + 0x18, 0x0D, 0xEA, 0xC2, 0x14, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xBC, 0xFF, 0xF1, 0x58, 0x80, 0x1B, 0x62, 0x90, 0x21, 0x4A, 0xCC, 0x80, 0x1F, 0xEA, 0xFE, 0xBF, 0xC8, + 0x87, 0x77, 0x07, 0x5B, 0x6B, 0x0A, 0xDA, 0x70, 0x6A, 0xC6, 0x8F, 0xE1, 0xE6, 0xC4, 0x36, 0x01, 0x48, 0xB1, 0x94, 0xF0, 0x67, 0x71, 0x13, 0x21, + 0x28, 0xB0, 0x74, 0xC1, 0x02, 0x0B, 0xED, 0x1B, 0x75, 0xF2, 0x1D, 0x89, 0x8A, 0xFA, 0xCB, 0x17, 0x0D, 0x51, 0xAE, 0x81, 0x2D, 0xB3, 0x27, 0x07, + 0x00, 0x69, 0x11, 0x1E, 0x51, 0x2E, 0x56, 0x4C, 0xD1, 0x9C, 0x39, 0x65, 0xED, 0x43, 0x53, 0x4E, 0x4E, 0x22, 0x8D, 0x5D, 0x48, 0x56, 0x50, 0x66, + 0x75, 0x4B, 0x3D, 0x6B, 0x10, 0xE5, 0x38, 0x97, 0x8D, 0x5C, 0xEC, 0xCC, 0x6F, 0xDA, 0x1D, 0xD5, 0x99, 0x0C, 0xBD, 0x50, 0xDB, 0x92, 0xDE, 0x06, + 0xD1, 0x0A, 0xE2, 0xD5, 0x32, 0x9C, 0xEA, 0x82, 0x1A, 0x30, 0x72, 0x92, 0xC1, 0x30, 0x88, 0xAC, 0x38, 0x15, 0xD9, 0xE7, 0x22, 0x5A, 0x97, 0x94, + 0x0D, 0xE4, 0xFF, 0xFF, 0xFC, 0xC8, 0xD0, 0xEE, 0xDB, 0x60, 0x26, 0x6D, 0x11, 0x0D, 0x69, 0x10, 0x61, 0x63, 0xA7, 0xE7, 0xB7, 0x58, 0x11, 0x63, + 0x29, 0xE0, 0xFB, 0xBD, 0x8E, 0x3C, 0x98, 0xBC, 0x01, 0x7E, 0x8F, 0x7B, 0xF7, 0xCB, 0x3A, 0xC2, 0xC0, 0xC2, 0xC2, 0xCB, 0x01, 0x1F, 0xA3, 0x94, + 0xE8, 0x28, 0x44, 0x40, 0x52, 0x10, 0x42, 0x48, 0x0A, 0x94, 0x53, 0x9D, 0x57, 0xB0, 0x4B, 0x3E, 0x50, 0x80, 0xDF, 0xD2, 0x70, 0x30, 0x95, 0x33, + 0x92, 0x2C, 0xAD, 0x9D, 0x59, 0x74, 0x83, 0xC0, 0x00, 0x00, 0x38, 0xFF, 0xF1, 0x58, 0x80, 0x16, 0xC2, 0x74, 0x21, 0x79, 0x08, 0x00, 0x87, 0xFF, + 0xFF, 0xCF, 0x11, 0x6C, 0xCC, 0x21, 0x92, 0x14, 0x02, 0x80, 0xE8, 0x05, 0x50, 0x10, 0x50, 0x3F, 0xC7, 0xC6, 0x67, 0x50, 0xB0, 0xBB, 0x63, 0x1A, + 0x77, 0x48, 0xF3, 0x0E, 0x0A, 0x02, 0x2F, 0xC6, 0xDA, 0xD2, 0xFA, 0xDA, 0x2D, 0x32, 0x15, 0x6E, 0xEE, 0xF0, 0x11, 0x7F, 0x0F, 0x13, 0xC9, 0x48, + 0xEB, 0x84, 0x2D, 0x63, 0xAD, 0xAD, 0x61, 0xCA, 0x43, 0x5E, 0x1A, 0xDC, 0x31, 0x76, 0x4B, 0x9C, 0xBB, 0xD1, 0xF7, 0xD4, 0x84, 0xF0, 0x35, 0x83, + 0x78, 0xEE, 0x95, 0x05, 0x2E, 0x61, 0x76, 0xDB, 0xE1, 0x68, 0xC9, 0x23, 0xA4, 0x1E, 0xAA, 0x71, 0xCA, 0x9A, 0xA5, 0xEE, 0x88, 0x0D, 0x0F, 0xE7, + 0xFE, 0x55, 0x6D, 0x0F, 0x61, 0xA3, 0xAE, 0xEE, 0xF3, 0x08, 0xCB, 0x13, 0x08, 0x28, 0x0A, 0x1E, 0x28, 0x40, 0x70, 0xA0, 0xA7, 0x84, 0x14, 0x15, + 0xF5, 0xD0, 0x50, 0x53, 0x51, 0x5F, 0x32, 0x89, 0xA8, 0xAC, 0x1F, 0x73, 0x62, 0xD1, 0x67, 0x53, 0x57, 0x86, 0x2C, 0x80, 0x44, 0xFB, 0xC2, 0xF4, + 0xAF, 0x24, 0x93, 0x1D, 0x40, 0x00, 0xFF, 0x89, 0x36, 0xFF, 0xC6, 0x45, 0x23, 0x7F, 0xF0, 0x5B, 0xCC, 0x00, 0x00, 0x00, 0x03, 0x20, 0x0C, 0x01, + 0x38, +}; constexpr std::int32_t errorApiFail = static_cast(0x807F0000); constexpr std::int32_t errorInvalidType = static_cast(0x807F0001); constexpr std::int32_t errorArg = static_cast(0x807F0002); @@ -225,6 +280,46 @@ void TestAacRaw51(std::uint32_t maxChannels) { Require(sceAudiodecDeleteDecoder(handle) == 0); } +double UpperHalfEnergyShare(const std::int16_t* samples, std::size_t count, std::size_t stride) { + double upper = 0; + double total = 0; + for (std::size_t bin = 1; bin < count / 2; ++bin) { + double real = 0; + double imaginary = 0; + for (std::size_t i = 0; i < count; ++i) { + const double phase = 2 * std::numbers::pi * static_cast(bin * i % count) / static_cast(count); + real += samples[i * stride] * std::cos(phase); + imaginary += samples[i * stride] * std::sin(phase); + } + total += real * real + imaginary * imaginary; + if (bin >= count / 4) upper += real * real + imaginary * imaginary; + } + return upper / total; +} + +void TestHeAac(const std::uint8_t* stream, std::size_t streamSize, std::uint32_t channels, bool adts, bool highEfficiency) { + AudiodecParamM4aac param{sizeof(param), 1, adts ? 1u : 2u, 6, channels, highEfficiency ? 1u : 0u}; + AudiodecM4aacInfo info{sizeof(info), 0, 0, 0, 0}; + AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; + const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 3); + Require(handle > 0); + Streams streams; + const std::uint32_t samples = highEfficiency ? 2048 : 1024; + const std::uint32_t room = samples * channels * 2; + for (std::size_t offset = 0; offset < streamSize; offset += AdtsLength(stream + offset)) { + const std::size_t length = AdtsLength(stream + offset); + Require(Decode(handle, ctrl, streams, adts ? stream + offset : stream + offset + 7, adts ? length : length - 7, room) == 0); + Require(streams.au.ui_au_size == (adts ? length : length - 7)); + Require(streams.pcm.ui_pcm_size == room && info.i_result == 0); + Require(info.ui_sampling_freq == (highEfficiency ? 48000u : 24000u) && info.ui_number_of_channels == channels); + } + if (highEfficiency) { + Require(info.ui_heaac == 1); + Require(UpperHalfEnergyShare(reinterpret_cast(streams.output.data()), samples, channels) > 0.02); + } + Require(sceAudiodecDeleteDecoder(handle) == 0); +} + void TestMp3() { AudiodecParamMp3 param{sizeof(param), 2}; AudiodecMp3Info info{}; @@ -315,6 +410,12 @@ int main() { TestAac(false, 8); TestAacRaw51(6); TestAacRaw51(8); + for (const bool adts : {true, false}) { + for (const bool highEfficiency : {true, false}) { + TestHeAac(HEAAC_V2_MONO, sizeof(HEAAC_V2_MONO), 1, adts, highEfficiency); + TestHeAac(HEAAC_STEREO, sizeof(HEAAC_STEREO), 2, adts, highEfficiency); + } + } TestMp3(); TestAt9(); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecTermLibrary(type) == 0); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 0410bbab96..5e4b500e60 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -72,6 +73,7 @@ constexpr std::int32_t EventPlay = 3; constexpr std::int32_t EventPause = 4; constexpr std::int32_t EventWarning = 0x20; const char* const MoviePath = "app0/avplayer.mp4"; +const char* const WebmPath = "app0/avplayer.webm"; constexpr std::array Movie{ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00, @@ -451,6 +453,109 @@ constexpr std::array Movie{ 0x5b, 0xdc }; +constexpr std::array WebmMovie{ + 0x1a, 0x45, 0xdf, 0xa3, 0x9f, 0x42, 0x86, 0x81, 0x01, 0x42, 0xf7, 0x81, 0x01, 0x42, 0xf2, 0x81, + 0x04, 0x42, 0xf3, 0x81, 0x08, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d, 0x42, 0x87, 0x81, 0x04, + 0x42, 0x85, 0x81, 0x02, 0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x0a, + 0x11, 0x4d, 0x9b, 0x74, 0xba, 0x4d, 0xbb, 0x8b, 0x53, 0xab, 0x84, 0x15, 0x49, 0xa9, 0x66, 0x53, + 0xac, 0x81, 0xa1, 0x4d, 0xbb, 0x8b, 0x53, 0xab, 0x84, 0x16, 0x54, 0xae, 0x6b, 0x53, 0xac, 0x81, + 0xc6, 0x4d, 0xbb, 0x8c, 0x53, 0xab, 0x84, 0x12, 0x54, 0xc3, 0x67, 0x53, 0xac, 0x82, 0x01, 0x79, + 0x4d, 0xbb, 0x8c, 0x53, 0xab, 0x84, 0x1c, 0x53, 0xbb, 0x6b, 0x53, 0xac, 0x82, 0x05, 0xf4, 0xec, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x15, 0x49, 0xa9, 0x66, 0xa0, 0x2a, 0xd7, 0xb1, 0x83, 0x0f, 0x42, 0x40, 0x4d, 0x80, 0x84, + 0x4c, 0x61, 0x76, 0x66, 0x57, 0x41, 0x84, 0x4c, 0x61, 0x76, 0x66, 0x44, 0x89, 0x88, 0x40, 0x6a, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x54, 0xae, 0x6b, 0x40, 0xad, 0xae, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3f, 0xd7, 0x81, 0x01, 0x73, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x9c, 0x81, 0x00, 0x22, 0xb5, 0x9c, 0x83, 0x65, 0x6e, 0x67, 0x88, 0x81, 0x00, + 0x86, 0x85, 0x56, 0x5f, 0x56, 0x50, 0x39, 0x83, 0x81, 0x01, 0x23, 0xe3, 0x83, 0x84, 0x01, 0xfc, + 0xa0, 0x55, 0xe0, 0x90, 0xb0, 0x81, 0x64, 0xba, 0x81, 0x3c, 0x9a, 0x81, 0x02, 0x55, 0xb0, 0x84, + 0x55, 0xb9, 0x81, 0x01, 0xae, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0xd7, 0x81, 0x02, + 0x73, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x9c, 0x81, 0x00, 0x22, 0xb5, + 0x9c, 0x83, 0x65, 0x6e, 0x67, 0x88, 0x81, 0x00, 0x86, 0x86, 0x41, 0x5f, 0x4f, 0x50, 0x55, 0x53, + 0x56, 0xaa, 0x83, 0x63, 0x2e, 0xa0, 0x56, 0xbb, 0x84, 0x04, 0xc4, 0xb4, 0x00, 0x83, 0x81, 0x02, + 0xe1, 0x91, 0x9f, 0x81, 0x02, 0xb5, 0x88, 0x40, 0xe7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, + 0x64, 0x81, 0x10, 0x63, 0xa2, 0x93, 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, + 0x38, 0x01, 0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x54, 0xc3, 0x67, 0x40, 0xa5, 0x73, + 0x73, 0xd1, 0x63, 0xc0, 0x8b, 0x63, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x67, 0xc8, 0x9c, 0x45, 0xa3, 0x87, 0x45, 0x4e, 0x43, 0x4f, 0x44, 0x45, 0x52, 0x44, 0x87, 0x8f, + 0x4c, 0x61, 0x76, 0x63, 0x20, 0x6c, 0x69, 0x62, 0x76, 0x70, 0x78, 0x2d, 0x76, 0x70, 0x39, 0x67, + 0xc8, 0xa1, 0x45, 0xa3, 0x88, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x44, 0x87, 0x93, + 0x30, 0x30, 0x3a, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x00, 0x73, 0x73, 0xce, 0x63, 0xc0, 0x8b, 0x63, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x67, 0xc8, 0x99, 0x45, 0xa3, 0x87, 0x45, 0x4e, 0x43, 0x4f, 0x44, 0x45, + 0x52, 0x44, 0x87, 0x8c, 0x4c, 0x61, 0x76, 0x63, 0x20, 0x6c, 0x69, 0x62, 0x6f, 0x70, 0x75, 0x73, + 0x67, 0xc8, 0xa1, 0x45, 0xa3, 0x88, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x44, 0x87, + 0x93, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x38, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x00, 0x1f, 0x43, 0xb6, 0x75, 0x43, 0xca, 0xe7, 0x81, 0x00, 0xa3, 0x87, 0x82, + 0x00, 0x00, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0xb0, 0x81, 0x00, 0x00, 0x80, 0x82, 0x49, 0x83, 0x42, + 0x00, 0x06, 0x30, 0x03, 0xb6, 0x00, 0x38, 0x24, 0x1c, 0x18, 0x00, 0x00, 0x00, 0x60, 0x7d, 0x8f, + 0x18, 0xc0, 0x00, 0x00, 0x78, 0xb6, 0x32, 0x06, 0x00, 0x15, 0x8b, 0x45, 0x38, 0x14, 0x97, 0x0e, + 0x63, 0x08, 0xb7, 0x40, 0x1a, 0xde, 0xd0, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x15, 0x80, 0xfc, 0xff, + 0xfe, 0xa3, 0x40, 0x9f, 0x81, 0x00, 0x21, 0x00, 0x86, 0x00, 0x40, 0x92, 0xf0, 0x01, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x60, 0x5c, 0x40, 0x00, 0x00, 0x00, 0x02, 0x09, 0x3d, 0x02, + 0x8e, 0x7f, 0xb0, 0xdb, 0xaa, 0x7f, 0xff, 0xe7, 0x42, 0xdf, 0xdf, 0xe1, 0x6f, 0x3f, 0xff, 0xf3, + 0xa1, 0x6f, 0xef, 0xf0, 0xb7, 0x9f, 0xff, 0xf9, 0xd0, 0xb7, 0xf7, 0xf8, 0x5b, 0xcf, 0xff, 0xfc, + 0xe8, 0x5b, 0xfb, 0xfc, 0x2d, 0xe7, 0xff, 0xfe, 0x74, 0x2d, 0xfd, 0xfe, 0x16, 0xf3, 0xff, 0xff, + 0x3a, 0x16, 0xfe, 0xff, 0x0b, 0x79, 0xff, 0xff, 0x9d, 0x0b, 0x7f, 0x7f, 0x85, 0x81, 0xcc, 0xbc, + 0xff, 0xff, 0xce, 0x85, 0xbf, 0xbf, 0xc2, 0xde, 0x7f, 0xff, 0xe7, 0x42, 0xdf, 0xdf, 0xe1, 0x6f, + 0x3f, 0xff, 0xf3, 0xa1, 0x6f, 0xef, 0xf0, 0xb7, 0x9f, 0xff, 0xf9, 0xd0, 0xb7, 0xf7, 0xf8, 0x5b, + 0xcf, 0xff, 0xfc, 0xe8, 0x5b, 0xfb, 0xfc, 0x2d, 0xe7, 0xff, 0xfe, 0x74, 0x2d, 0xfd, 0xfe, 0x16, + 0xf3, 0xff, 0xff, 0x3a, 0x16, 0xfe, 0xff, 0x0b, 0x79, 0xff, 0xff, 0x9d, 0x0b, 0x7f, 0x7f, 0x85, + 0x80, 0x93, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x29, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x87, 0x82, 0x00, + 0x3d, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x40, 0x9f, 0x81, 0x00, 0x43, 0x00, 0x86, 0x00, 0x40, 0x92, + 0xf0, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x58, 0xc6, 0x80, 0x00, 0x00, 0x00, + 0x04, 0x12, 0x79, 0xde, 0x97, 0xff, 0x05, 0xc3, 0x79, 0xff, 0xff, 0x8f, 0xe5, 0xff, 0x82, 0xe1, + 0xbc, 0xff, 0xff, 0xc7, 0xf2, 0xff, 0xc1, 0x70, 0xde, 0x7f, 0xff, 0xe3, 0xf9, 0x7f, 0xe0, 0xb8, + 0x6f, 0x3f, 0xff, 0xf1, 0xfc, 0xbf, 0xf0, 0x5c, 0x37, 0x9f, 0xff, 0xf8, 0xfe, 0x5f, 0xf8, 0x2e, + 0x1b, 0xcf, 0xff, 0xfc, 0x7f, 0x2f, 0xfc, 0x17, 0x0d, 0xe7, 0xff, 0xfe, 0x3f, 0x97, 0xfe, 0x0b, + 0x86, 0x07, 0x32, 0xf3, 0xff, 0xff, 0x1f, 0xcb, 0xff, 0x05, 0xc3, 0x79, 0xff, 0xff, 0x8f, 0xe5, + 0xff, 0x82, 0xe1, 0xbc, 0xff, 0xff, 0xc7, 0xf2, 0xff, 0xc1, 0x70, 0xde, 0x7f, 0xff, 0xe3, 0xf9, + 0x7f, 0xe0, 0xb8, 0x6f, 0x3f, 0xff, 0xf1, 0xfc, 0xbf, 0xf0, 0x5c, 0x37, 0x9f, 0xff, 0xf8, 0xfe, + 0x5f, 0xf8, 0x2e, 0x1b, 0xcf, 0xff, 0xfc, 0x7f, 0x2f, 0xfc, 0x17, 0x0d, 0xe7, 0xff, 0xfe, 0x3f, + 0x97, 0xfe, 0x0b, 0x86, 0x02, 0x4c, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x51, 0x80, 0xfc, 0xff, 0xfe, + 0xa3, 0x87, 0x82, 0x00, 0x65, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x40, 0x9d, 0x81, 0x00, 0x64, 0x00, + 0x86, 0x00, 0x40, 0x92, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x4c, 0xe5, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x24, 0xf3, 0x40, 0x0f, 0xfb, 0xaa, 0xcb, 0xcf, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x80, 0x27, 0xfc, 0x9f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x80, 0x0c, 0xc0, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x79, 0x80, 0xfc, + 0xff, 0xfe, 0xa3, 0x40, 0x9f, 0x81, 0x00, 0x85, 0x00, 0x86, 0x00, 0x40, 0x92, 0xf0, 0x01, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x37, 0x9e, 0x40, 0x00, 0x00, 0x00, 0x02, 0x09, 0x3c, + 0x9e, 0x10, 0x3f, 0xa7, 0x97, 0xf2, 0x3f, 0xff, 0xee, 0x24, 0x7f, 0xd7, 0xcf, 0x67, 0x9f, 0xff, + 0xf7, 0x12, 0x3f, 0xeb, 0xe7, 0xb3, 0xcf, 0xff, 0xfb, 0x89, 0x1f, 0xf5, 0xf3, 0xd9, 0xe7, 0xff, + 0xfd, 0xc4, 0x8f, 0xfa, 0xf9, 0xec, 0xf3, 0xff, 0xfe, 0xe2, 0x47, 0xfd, 0x7c, 0xf6, 0x79, 0xff, + 0xff, 0x71, 0x23, 0xfe, 0xbe, 0x7b, 0x3c, 0xff, 0xff, 0xb8, 0x91, 0xff, 0x5f, 0x3d, 0x80, 0xe6, + 0x5e, 0x7f, 0xff, 0xdc, 0x48, 0xff, 0xaf, 0x9e, 0xcf, 0x3f, 0xff, 0xee, 0x24, 0x7f, 0xd7, 0xcf, + 0x67, 0x9f, 0xff, 0xf7, 0x12, 0x3f, 0xeb, 0xe7, 0xb3, 0xcf, 0xff, 0xfb, 0x89, 0x1f, 0xf5, 0xf3, + 0xd9, 0xe7, 0xff, 0xfd, 0xc4, 0x8f, 0xfa, 0xf9, 0xec, 0xf3, 0xff, 0xfe, 0xe2, 0x47, 0xfd, 0x7c, + 0xf6, 0x79, 0xff, 0xff, 0x71, 0x23, 0xfe, 0xbe, 0x7b, 0x3c, 0xff, 0xff, 0xb8, 0x91, 0xff, 0x5f, + 0x3d, 0x80, 0x49, 0x80, 0xa3, 0x87, 0x82, 0x00, 0x8d, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x87, 0x82, + 0x00, 0xa1, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x40, 0x9d, 0x81, 0x00, 0xa7, 0x00, 0x86, 0x00, 0x40, + 0x92, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x31, 0x74, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x12, 0x79, 0x1a, 0xe4, 0xfe, 0x8f, 0x1c, 0x91, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0x05, 0xc7, 0x67, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0x01, 0xd4, 0x00, 0xa3, 0x87, 0x82, 0x00, 0xb5, 0x80, 0xfc, 0xff, 0xfe, 0xa0, + 0x93, 0xa1, 0x87, 0x82, 0x00, 0xc9, 0x00, 0xfc, 0xff, 0xfe, 0x9b, 0x81, 0x07, 0x75, 0xa2, 0x84, + 0x00, 0xcd, 0xfe, 0x60, 0x1c, 0x53, 0xbb, 0x6b, 0x91, 0xbb, 0x8f, 0xb3, 0x81, 0x00, 0xb7, 0x8a, + 0xf7, 0x81, 0x01, 0xf1, 0x82, 0x02, 0x24, 0xf0, 0x81, 0x0c, +}; + void Check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } @@ -543,6 +648,11 @@ struct Events { return std::any_of(received.begin(), received.end(), [&](const auto& event) { return event.first == id && event.second == warning; }); } + int Count(std::int32_t id, std::int32_t warning = 0) { + std::lock_guard lock(mutex); + return static_cast(std::count_if(received.begin(), received.end(), [&](const auto& event) { return event.first == id && event.second == warning; })); + } + void Clear() { std::lock_guard lock(mutex); received.clear(); @@ -829,7 +939,7 @@ void TestPlayback() { } return wrapped && frames > FrameCount + 5; }), "looping never wrapped"); - Check(events.Seen(EventWarning, LoopingBack), "looping warning missing"); + Check(WaitFor([&] { return events.Seen(EventWarning, LoopingBack); }), "looping warning missing"); Check(sceAvPlayerIsActive(player), "looping player went inactive"); Check(sceAvPlayerStop(player) == 0, "stop failed"); Check(sceAvPlayerStop(player) == OperationFailed, "second stop accepted"); @@ -842,6 +952,46 @@ void TestPlayback() { Check(allocations.textures >= 4, "video buffers not allocated as textures"); } +void TestVp9OpusPlayback() { + { + std::ofstream file(WebmPath, std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(WebmMovie.data()), static_cast(WebmMovie.size())); + Check(static_cast(file), "cannot write the WebM movie"); + } + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "WebM init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.webm") == 0, "WebM add source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "WebM ready event missing"); + Check(sceAvPlayerStreamCount(player) == 2, "unexpected WebM stream count"); + AvPlayerStreamInfo video{}; + Check(sceAvPlayerGetStreamInfo(player, 0, &video) == 0 && video.type == 1 && video.details.video.width == 112 && video.details.video.height == 64, "unexpected VP9 stream"); + AvPlayerStreamInfo audio{}; + Check(sceAvPlayerGetStreamInfo(player, 1, &audio) == 0 && audio.type == 0 && audio.details.audio.channel_count == 2 && audio.details.audio.sample_rate == SampleRate, "unexpected Opus stream"); + Check(sceAvPlayerEnableStream(player, 0) == 0 && sceAvPlayerEnableStream(player, 1) == 0, "enable WebM streams failed"); + Check(sceAvPlayerStart(player) == 0, "WebM start failed"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + int frames = 0; + int sounds = 0; + Check(WaitFor([&] { + if (sceAvPlayerGetAudioData(player, &sound)) { + CheckEnglishAudio(sound); + ++sounds; + } + if (sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + ++frames; + } + return !sceAvPlayerIsActive(player); + }), "WebM playback never ended"); + Check(frames >= 5 && sounds >= 2, "WebM delivered " + std::to_string(frames) + " frames and " + std::to_string(sounds) + " audio chunks"); + Check(sceAvPlayerClose(player) == 0, "WebM close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "WebM allocations leaked"); +} + void TestChangeStream() { Events events; AvPlayerInitData init = InitData(&events); @@ -1028,8 +1178,22 @@ void TestPs5ExtendedInitLayout() { AvPlayerInternal* player = nullptr; Check(sceAvPlayerInitEx(reinterpret_cast(raw.data()), &player) == 0 && player != nullptr, "PS5 extended init failed"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source after the PS5 extended init failed"); + AvPlayerStreamInfo audioInfo{}; + Check(sceAvPlayerGetStreamInfo(player, EnglishAudioStream, &audioInfo) == 0, "PS5 audio stream info failed"); + Check(audioInfo.type == 2, "PS5 audio stream must use type 2 so Unity enables it"); + AvPlayerStreamInfoEx audioInfoEx{}; + audioInfoEx.this_size = sizeof(audioInfoEx); + Check(sceAvPlayerGetStreamInfoEx(player, EnglishAudioStream, &audioInfoEx) == 0 && audioInfoEx.type == 2, "PS5 extended audio stream type is incorrect"); + AvPlayerStreamInfoEx videoInfo{}; + videoInfo.this_size = sizeof(videoInfo); + Check(sceAvPlayerGetStreamInfoEx(player, VideoStream, &videoInfo) == 0, "PS5 video stream info failed"); + double frameRate = 0; + std::memcpy(&frameRate, reinterpret_cast(&videoInfo) + 0x40, sizeof(frameRate)); + Check(std::fabs(frameRate - FrameRate) < 0.01, "PS5 extended video frame rate at 0x40 is missing"); if (autoStart) { Check(WaitFor([&] { return events.Seen(EventPlay); }), "auto start at 0x74 of the PS5 extended init did not start playback"); + AvPlayerFrameInfo audioFrame{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audioFrame) != 0; }), "PS5 auto start did not enable audio"); } else { Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing after the PS5 extended init"); std::this_thread::sleep_for(std::chrono::milliseconds(200)); @@ -1038,6 +1202,10 @@ void TestPs5ExtendedInitLayout() { Check(sceAvPlayerStart(player) == 0, "start after the PS5 extended init failed"); } Check(WaitFor([&] { return TextureCount() == Buffers + DecodeAheadBuffers; }), "video frame buffer count at 0x228 of the PS5 extended init not used"); + AvPlayerFrameInfoEx videoFrame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &videoFrame) != 0; }), "PS5 playback did not deliver video"); + std::memcpy(&frameRate, reinterpret_cast(&videoFrame) + 0x48, sizeof(frameRate)); + Check(std::fabs(frameRate - FrameRate) < 0.01, "PS5 video frame rate at 0x48 is missing"); Check(sceAvPlayerClose(player) == 0, "close after the PS5 extended init failed"); }; run(false); @@ -1123,6 +1291,159 @@ void TestUnsyncedVideoKeepsUpWithAudio() { Check(sceAvPlayerClose(player) == 0, "close failed"); } +void TestLoopWarningAtPlayback() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "loop timing init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "loop timing source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "loop timing ready missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "loop timing video failed"); + Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "loop timing sync mode failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "loop timing looping failed"); + Check(sceAvPlayerStart(player) == 0, "loop timing start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "loop timing play missing"); + AvPlayerFrameInfoEx frame{}; + for (int index = 0; index <= FrameCount * 3; ++index) { + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop timing frame missing"); + Check(FrameIndex(frame.timestamp) == index % FrameCount, "loop timing frame out of order"); + const int loops = index / FrameCount; + if (loops > 0) Check(WaitFor([&] { return events.Count(EventWarning, LoopingBack) >= loops; }), "loop timing warning missing"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + Check(events.Count(EventWarning, LoopingBack) == loops, "loop warning did not match the playback boundary"); + } + events.Clear(); + Check(sceAvPlayerSetLooping(player, 0) == 0, "loop timing disable looping failed"); + Check(sceAvPlayerJumpToTime(player, 0) == 0, "loop timing jump failed"); + Check(WaitFor([&] { return events.Seen(EventWarning, JumpComplete); }), "loop timing jump warning missing"); + for (int index = 0; index < FrameCount; ++index) { + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop timing seek frame missing"); + Check(FrameIndex(frame.timestamp) == index, "loop timing seek frame out of order"); + } + Check(sceAvPlayerStop(player) == 0, "loop timing stop failed"); + Check(WaitFor([&] { return events.Seen(EventStop); }), "loop timing stop event missing"); + Check(events.Count(EventWarning, LoopingBack) == 0, "seek delivered a pending loop warning"); + events.Clear(); + Check(sceAvPlayerStart(player) == 0, "loop timing restart failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "loop timing restart event missing"); + for (int index = 0; index < FrameCount; ++index) { + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop timing restart frame missing"); + Check(FrameIndex(frame.timestamp) == index, "loop timing restart frame out of order"); + } + Check(sceAvPlayerStop(player) == 0, "loop timing restart stop failed"); + Check(WaitFor([&] { return events.Seen(EventStop); }), "loop timing restart stop event missing"); + Check(events.Count(EventWarning, LoopingBack) == 0, "restart delivered a pending loop warning"); + Check(sceAvPlayerClose(player) == 0, "loop timing close failed"); +} + +void TestAudioLoopWarningAtPlayback() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "audio loop timing init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "audio loop timing source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "audio loop timing ready missing"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "audio loop timing stream failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "audio loop timing looping failed"); + Check(sceAvPlayerStart(player) == 0, "audio loop timing start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "audio loop timing play missing"); + AvPlayerFrameInfo frame{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &frame) != 0; }), "audio loop timing first frame missing"); + int loops = 0; + for (int index = 0; index < 256 && loops < 3; ++index) { + const auto previous = frame.timestamp; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &frame) != 0; }), "audio loop timing frame missing"); + if (frame.timestamp < previous) ++loops; + if (loops > 0) Check(WaitFor([&] { return events.Count(EventWarning, LoopingBack) >= loops; }), "audio loop timing warning missing"); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + Check(events.Count(EventWarning, LoopingBack) == loops, "audio loop warning did not match the playback boundary"); + } + Check(loops == 3, "audio loop timing did not wrap three times"); + Check(sceAvPlayerClose(player) == 0, "audio loop timing close failed"); +} + +void TestLoopWarningSharedByStreams() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "shared loop timing init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "shared loop timing source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "shared loop timing ready missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "shared loop timing video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "shared loop timing audio failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "shared loop timing looping failed"); + Check(sceAvPlayerStart(player) == 0, "shared loop timing start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "shared loop timing play missing"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + std::uint64_t lastVideo = 0; + std::uint64_t lastAudio = 0; + bool videoWrapped = false; + bool audioWrapped = false; + Check(WaitFor([&] { + if (!audioWrapped && sceAvPlayerGetAudioData(player, &sound)) { + CheckEnglishAudio(sound); + audioWrapped = sound.timestamp < lastAudio; + lastAudio = sound.timestamp; + } + if (!videoWrapped && sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + videoWrapped = frame.timestamp < lastVideo; + lastVideo = frame.timestamp; + } + return audioWrapped && videoWrapped; + }), "shared loop timing streams did not both wrap"); + Check(sceAvPlayerPause(player) == 0, "shared loop timing pause failed"); + Check(WaitFor([&] { return events.Seen(EventPause); }), "shared loop timing pause event missing"); + Check(events.Count(EventWarning, LoopingBack) == 1, "audio and video reported the same loop more than once"); + Check(sceAvPlayerClose(player) == 0, "shared loop timing close failed"); +} + +void TestNoSyncVideoCanLeadAudioClock() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "no-sync init failed"); + Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "no-sync mode rejected"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "no-sync source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "no-sync video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "no-sync audio failed"); + Check(sceAvPlayerStart(player) == 0, "no-sync start failed"); + AvPlayerFrameInfo audio{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audio) != 0; }), "no-sync audio unavailable"); + std::uint64_t lead = 0; + const bool available = WaitFor([&] { + AvPlayerFrameInfoEx video{}; + if (!sceAvPlayerGetVideoDataEx(player, &video)) return false; + CheckVideoFrame(video); + const auto clock = sceAvPlayerCurrentTime(player); + if (video.timestamp > clock) lead = std::max(lead, video.timestamp - clock); + return lead >= 100; + }, 500); + Check(sceAvPlayerClose(player) == 0, "no-sync close failed"); + std::printf("No-sync video lead: %llu ms\n", static_cast(lead)); + Check(available, "no-sync video was gated by the audio-backed player clock"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "no-sync allocations leaked"); +} + +void TestLoopWarningWaitsForPlayback() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "loop warning init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "loop warning source failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "loop warning setting failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "loop warning video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "loop warning audio failed"); + Check(sceAvPlayerStart(player) == 0, "loop warning start failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop warning first frame missing"); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Check(!events.Seen(EventWarning, LoopingBack), "loop warning arrived while the first playback still had buffered audio and video"); + Check(sceAvPlayerClose(player) == 0, "loop warning close failed"); +} + void TestEofStopLatency() { constexpr int Rounds = 9; std::array elapsed{}; @@ -1178,12 +1499,75 @@ void TestEofStopLatency() { Check(median < std::chrono::milliseconds(10), "eof stop median " + std::to_string(medianMs) + " ms exceeds 10 ms"); } +struct ClosingEvents { + std::mutex mutex; + AvPlayerInternal* player = nullptr; + AvPlayerInternal* replacement = nullptr; + std::int32_t closeEvent; + bool closed = false; +}; + +void APS5_VABI CloseOnEvent(void* object, std::int32_t id, std::int32_t, void*) { + auto& events = *static_cast(object); + if (id != events.closeEvent) return; + std::lock_guard lock(events.mutex); + Check(sceAvPlayerClose(events.player) == 0, "close from event callback failed"); + events.player = nullptr; + AvPlayerInitData init = InitData(nullptr); + events.replacement = sceAvPlayerInit(&init); + Check(events.replacement != nullptr, "replacement player init from callback failed"); + events.closed = true; } -int main() { +void TestCloseFromEventCallback(std::int32_t event) { + ClosingEvents events; + events.closeEvent = event; + AvPlayerInitData init = InitData(nullptr); + init.event_replacement = {&events, CloseOnEvent}; + init.auto_start = event == EventStop; + { + std::lock_guard lock(events.mutex); + events.player = sceAvPlayerInit(&init); + Check(events.player != nullptr, "callback-close init failed"); + Check(sceAvPlayerAddSource(events.player, "/app0/avplayer.mp4") == 0, "callback-close source failed"); + } + Check(WaitFor([&] { + std::lock_guard lock(events.mutex); + if (events.closed) return true; + if (event == EventStop) { + AvPlayerFrameInfoEx video{}; + AvPlayerFrameInfo audio{}; + if (sceAvPlayerGetVideoDataEx(events.player, &video)) CheckVideoFrame(video); + if (sceAvPlayerGetAudioData(events.player, &audio)) CheckEnglishAudio(audio); + } + return false; + }), "event callback did not finish closing the player"); + { + std::lock_guard lock(events.mutex); + Check(sceAvPlayerClose(events.replacement) == 0, "replacement player close failed"); + } + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "callback close leaked media allocations"); +} + +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::strcmp(argv[1], "--callback-close") == 0) { + WriteMovie(); + TestCloseFromEventCallback(EventReady); + TestCloseFromEventCallback(EventStop); + std::puts("AvPlayer callback close tests passed"); + return 0; + } + Check(argc == 1, "unknown AvPlayer test arguments"); WriteMovie(); + TestLoopWarningAtPlayback(); + TestAudioLoopWarningAtPlayback(); + TestLoopWarningSharedByStreams(); TestPlayback(); + TestVp9OpusPlayback(); TestChangeStream(); TestExtendedExports(); TestWithoutAllocators(); @@ -1194,6 +1578,8 @@ int main() { TestAutoStartKeepsStartedPlayback(); TestRestartAfterPausedJumpKeepsClockRunning(); TestUnsyncedVideoKeepsUpWithAudio(); + TestNoSyncVideoCanLeadAudioClock(); + TestLoopWarningWaitsForPlayback(); TestEofStopLatency(); std::puts("AvPlayer tests passed"); return 0; diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index 76b09054a6..31775180e5 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -28,6 +28,7 @@ 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); +int APS5_VABI sceKernelGetCurrentCpu(void); } static constexpr int SCE_OK = 0; @@ -68,10 +69,28 @@ static void* APS5_VABI Worker(void* arg) { return nullptr; } +struct RemoteThread { + std::atomic tid{0}; + std::future* release = nullptr; + KernelCpumask observed = 0; + KernelCpumask inherited = 0; + int cpu = -1; +}; + +static void* APS5_VABI RecordOwnAffinity(void* arg) { + Require(scePthreadGetaffinity(scePthreadSelf(), static_cast(arg)) == SCE_OK); + 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(); + auto& data = *static_cast(arg); + data.tid.store(scePthreadGetthreadid(), std::memory_order_release); + data.release->get(); + Require(scePthreadGetaffinity(scePthreadSelf(), &data.observed) == SCE_OK); + data.cpu = sceKernelGetCurrentCpu(); + Pthread child = nullptr; + Require(scePthreadCreate(&child, nullptr, RecordOwnAffinity, &data.inherited, nullptr) == SCE_OK); + Require(scePthreadJoin(child, nullptr) == SCE_OK); return nullptr; } @@ -312,15 +331,34 @@ int main() { Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 4, -1, 8, processSet); })); std::promise holdOther; auto heldOther = holdOther.get_future(); - std::atomic otherTid{0}; + RemoteThread otherArg; + otherArg.release = &heldOther; 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); })); + while (otherArg.tid.load(std::memory_order_acquire) == 0) std::this_thread::yield(); + const int otherTid = otherArg.tid.load(std::memory_order_acquire); + std::uint64_t remoteSet[5] = {~0ull, ~0ull, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, GUEST_CPUSET_BYTES, remoteSet) == 0); + Require(remoteSet[0] == available && remoteSet[1] == 0 && remoteSet[2] == 0 && remoteSet[3] == 0 && remoteSet[4] == ~0ull); + const std::uint64_t remoteMask[4] = {last | (1ull << 40), 0, 0, 0}; + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(remoteMask), remoteMask) == 0); + Require(ReadCpuset(otherThread) == last); + Require(ReadCpuset(pthread_self_nid_postfix()) == available); + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, 8, remoteSet) == 0 && remoteSet[0] == last); + const std::uint64_t invalidRemote[4] = {first, 0, 0, 1}; + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(invalidRemote), invalidRemote) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + const std::uint64_t unavailableRemote[1] = {1ull << 40}; + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(unavailableRemote), unavailableRemote) == -1 && *__error_nid_postfix() == GUEST_EDEADLK); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 8, nullptr) == -1 && *__error_nid_postfix() == GUEST_EFAULT); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 4, remoteMask) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + Require(ReadCpuset(otherThread) == last); + Require(cpuset_getaffinity_nid_postfix(99, 1, otherTid, 8, remoteSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(99, 1, otherTid, 8, remoteMask) == -1 && *__error_nid_postfix() == GUEST_EINVAL); 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); + Require(otherArg.observed == last && otherArg.inherited == last); + Require(otherArg.cpu >= 0 && otherArg.cpu < PS5_LOGICAL_CPUS && (last & (KernelCpumask{1} << otherArg.cpu)) != 0); + remoteSet[0] = ~0ull; + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, 8, remoteSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH && remoteSet[0] == ~0ull); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 8, remoteMask) == -1 && *__error_nid_postfix() == GUEST_ESRCH); } diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp new file mode 100644 index 0000000000..25e958cdb9 --- /dev/null +++ b/core/libs/tests/GuestBacktrace.cpp @@ -0,0 +1,116 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); +} + +namespace { + +constexpr std::size_t NameOffset = 0x10; +constexpr unsigned char Fill = 0xcc; + +struct Record { + const std::byte* next; + std::uint32_t offset; + std::uint32_t reserved; + std::string module; + std::size_t bytes; +}; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "guest backtrace: %s\n", message); + std::abort(); +} + +std::vector Parse(const std::vector& buffer, std::size_t used, int count) { + std::vector records; + const auto* base = reinterpret_cast(buffer.data()); + const auto* cursor = count == 0 ? nullptr : base; + while (cursor != nullptr) { + Require(cursor >= base && static_cast(cursor - base) + NameOffset < used, "record outside the written bytes"); + Require(reinterpret_cast(cursor) % 8 == 0, "record is not 8-byte aligned"); + Record record{}; + std::memcpy(&record.next, cursor, sizeof(record.next)); + std::memcpy(&record.offset, cursor + 8, sizeof(record.offset)); + std::memcpy(&record.reserved, cursor + 12, sizeof(record.reserved)); + const auto* name = reinterpret_cast(cursor + NameOffset); + const auto limit = used - static_cast(cursor - base) - NameOffset; + const auto length = strnlen(name, limit); + Require(length < limit, "module name is not terminated inside the record"); + record.module.assign(name, length); + record.bytes = (NameOffset + length + 1 + 7) & ~std::size_t{7}; + Require(record.next == nullptr || record.next == cursor + record.bytes, "next does not point to the following record"); + records.push_back(record); + cursor = record.next; + } + Require(static_cast(records.size()) == count, "record list length differs from the reported count"); + return records; +} + +std::size_t Used(const std::vector& records) { + std::size_t total = 0; + for (const auto& record : records) total += record.bytes; + return total; +} + +} + +[[gnu::noinline]] void CheckBacktrace() { + Dl_info self{}; + Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); + std::string executable = self.dli_fname; + executable.erase(0, executable.find_last_of('/') + 1); + + int count = -1; + std::size_t size = 0; + Require(sceLibcBacktraceGetBufferSize_nid_postfix(-1, &count, &size) == 0, "GetBufferSize failed"); + Require(count > 1 && size >= static_cast(count) * NameOffset, "GetBufferSize reported too little"); + + std::vector buffer(size + 64, Fill); + int written = -1; + Require(sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written) == 0, "Self failed"); + Require(written == count, "Self wrote a different count than GetBufferSize reported"); + const auto records = Parse(buffer, size, written); + Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); + for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); + for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); + Require(records.front().module == executable, "first frame is not in the calling module"); + Dl_info caller{}; + const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; + Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); + + for (const int limit : {0, 1, 2}) { + Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); + std::vector limited(size + 64, Fill); + Require(sceLibcBacktraceSelf_nid_postfix(limit, limited.data(), limited.size(), &written) == 0 && written == limit, "Self ignored the frame limit"); + const auto limitedRecords = Parse(limited, limited.size(), written); + Require(Used(limitedRecords) == size, "limited Self used a different size than GetBufferSize reported"); + for (int index = 0; index < limit; ++index) Require(limitedRecords[index].module == records[index].module, "limited frames differ from the full trace"); + } + + const auto first = records.front().bytes; + for (const std::size_t available : {std::size_t{0}, first - 1, first, first + records[1].bytes - 1, first + records[1].bytes}) { + std::vector small(available + 64, Fill); + written = -1; + Require(sceLibcBacktraceSelf_nid_postfix(-1, small.data(), available, &written) == 0, "Self with a small buffer failed"); + const int expected = available < first ? 0 : available < first + records[1].bytes ? 1 : 2; + Require(written == expected, "Self with a small buffer wrote the wrong number of records"); + const auto smallRecords = Parse(small, available, written); + Require(written == 0 || smallRecords.back().next == nullptr, "last record is not terminated"); + for (std::size_t index = Used(smallRecords); index < small.size(); ++index) Require(small[index] == Fill, "Self wrote past the last record that fits"); + } +} + +int main() { + CheckBacktrace(); +} diff --git a/core/libs/tests/GuestBinaryWord.cpp b/core/libs/tests/GuestBinaryWord.cpp new file mode 100644 index 0000000000..2720a1f894 --- /dev/null +++ b/core/libs/tests/GuestBinaryWord.cpp @@ -0,0 +1,141 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI getw_nid_postfix(FileStream*); +int APS5_VABI putw_nid_postfix(int, FileStream*); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(FileStream*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftell_nid_postfix(FileStream*); +int APS5_VABI ungetc_nid_postfix(int, FileStream*); +int APS5_VABI fgetc_nid_postfix(FileStream*); +void APS5_VABI clearerr_nid_postfix(FileStream*); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); +} + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "binary word I/O: %s\n", message); + std::abort(); + } +} + +void CheckRoundTrip(const char* path) { + constexpr std::array values{0, 1, -1, std::numeric_limits::min(), std::numeric_limits::max(), + 0x12345678, 0x0d0a1a00, -1234567}; + auto* stream = fopen_nid_postfix(path, "w+b"); + Require(stream != nullptr, "open round-trip file"); + for (const int value : values) Require(putw_nid_postfix(value, stream) == 0, "putw returns zero on success"); + Require(ftell_nid_postfix(stream) == static_cast(4 * values.size()), "write exactly four bytes per word"); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0, "rewind written words"); + std::array bytes; + Require(fread_nid_postfix(bytes.data(), 1, bytes.size(), stream) == bytes.size(), "read raw word bytes"); + for (std::size_t word = 0; word < values.size(); ++word) + for (unsigned byte = 0; byte < 4; ++byte) + Require(bytes[4 * word + byte] == ((static_cast(values[word]) >> (8 * byte)) & 0xff), + "guest little-endian representation"); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0, "rewind for getw"); + for (const int value : values) { + Require(getw_nid_postfix(stream) == value, "read signed word including minus one"); + Require((stream->GuestState().flags & 0x60) == 0, "a stored minus one is not EOF or an error"); + } + Require(getw_nid_postfix(stream) == -1, "EOF returns minus one"); + Require((stream->GuestState().flags & 0x60) == 0x20, "publish EOF without an error"); + Require(fseek_nid_postfix(stream, 20, SEEK_SET) == 0, "seek to a buffered word"); + Require(fgetc_nid_postfix(stream) == 0x78, "read first word byte"); + Require(ungetc_nid_postfix(0x78, stream) == 0x78, "push back first word byte"); + Require(getw_nid_postfix(stream) == 0x12345678, "getw consumes pushed-back bytes"); + Require(ftell_nid_postfix(stream) == 24, "position after pushed-back word"); + Require(fclose_nid_postfix(stream) == 0, "close round-trip file"); +} + +void CheckShortReads(const char* path) { + constexpr std::array bytes{0xff, 0xaa, 0xbb}; + for (std::size_t length = 0; length <= bytes.size(); ++length) { + auto* stream = fopen_nid_postfix(path, "w+b"); + Require(stream != nullptr, "open short word file"); + Require(fwrite_nid_postfix(bytes.data(), 1, length, stream) == length, "write incomplete word"); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0, "rewind incomplete word"); + Require(getw_nid_postfix(stream) == -1, "incomplete word is EOF"); + Require((stream->GuestState().flags & 0x60) == 0x20, "short-read EOF flag"); + Require(ftell_nid_postfix(stream) == static_cast(length), "consume incomplete word bytes"); + clearerr_nid_postfix(stream); + Require((stream->GuestState().flags & 0x60) == 0, "clear short-read status"); + Require(fclose_nid_postfix(stream) == 0, "close incomplete word"); + } +} + +void CheckErrors(const char* path) { + auto* writer = fopen_nid_postfix(path, "wb"); + Require(writer != nullptr, "open write-only file"); + Require(getw_nid_postfix(writer) == -1, "read on write-only stream fails"); + Require((writer->GuestState().flags & 0x60) == 0x40, "publish read error, not EOF"); + Require(fclose_nid_postfix(writer) == 0, "close write-only file"); + auto* reader = fopen_nid_postfix(path, "rb"); + Require(reader != nullptr, "open read-only file"); + Require(putw_nid_postfix(0x12345678, reader) == -1, "write on read-only stream fails"); + Require((reader->GuestState().flags & 0x60) == 0x40, "publish write error"); + Require(fclose_nid_postfix(reader) == 0, "close read-only file"); + FileStream closed(std::tmpfile()); + Require(closed.Close() == 0, "close temporary stream"); + errno = 0; + Require(getw_nid_postfix(&closed) == -1 && errno == 9, "closed read reports EBADF"); + errno = 0; + Require(putw_nid_postfix(1, &closed) == -1 && errno == 9, "closed write reports EBADF"); + bool readThrew = false, writeThrew = false; + try { getw_nid_postfix(nullptr); } catch (const std::runtime_error&) { readThrew = true; } + try { putw_nid_postfix(1, nullptr); } catch (const std::invalid_argument&) { writeThrew = true; } + Require(readThrew && writeThrew, "null streams throw"); +} + +void CheckConcurrentWrites(const char* path) { + auto* stream = fopen_nid_postfix(path, "w+b"); + Require(stream != nullptr, "open shared stream"); + flockfile_nid_postfix(stream); + Require(putw_nid_postfix(0, stream) == 0, "putw accepts a lock held by its caller"); + funlockfile_nid_postfix(stream); + const auto writer = [stream](int value) { + for (int count = 0; count < 256; ++count) + Require(putw_nid_postfix(value, stream) == 0, "concurrent word write"); + }; + std::thread first(writer, 0x11223344), second(writer, 0x55667788); + first.join(); + second.join(); + Require(fseek_nid_postfix(stream, 4, SEEK_SET) == 0, "rewind concurrent words"); + int firstCount = 0, secondCount = 0; + for (int count = 0; count < 512; ++count) { + const int value = getw_nid_postfix(stream); + Require(value == 0x11223344 || value == 0x55667788, "concurrent words do not interleave their bytes"); + if (value == 0x11223344) ++firstCount; + else ++secondCount; + } + Require(firstCount == 256 && secondCount == 256, "both writers preserve every word"); + Require(fclose_nid_postfix(stream) == 0, "close shared stream"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected temporary file path"); + CheckRoundTrip(argv[1]); + CheckShortReads(argv[1]); + CheckErrors(argv[1]); + CheckConcurrentWrites(argv[1]); + Require(std::remove(argv[1]) == 0, "remove temporary file"); + std::puts("binary word I/O checks passed"); +} diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp index ddaf978819..7dec7aabab 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -11,6 +11,7 @@ extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); } namespace { @@ -63,6 +64,142 @@ std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& te return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); } +// collate hands the vtable pointers to its 16-bit characters, so the tests below pass the +// characters of a wide string where the vtable declares a range of char and count it in characters. +const char* Characters(const char16_t* text) { + return reinterpret_cast(text); +} + +int CompareWide(const GuestLocale::CollateFacet* facet, const std::u16string& left, const std::u16string& right) { + return Vtable(&facet->base).compare(facet, Characters(left.data()), Characters(left.data() + left.size()), Characters(right.data()), Characters(right.data() + right.size())); +} + +std::uint64_t HashWide(const GuestLocale::CollateFacet* facet, const std::u16string& text) { + return static_cast(Vtable(&facet->base).hash(facet, Characters(text.data()), Characters(text.data() + text.size()))); +} + +GuestLocale::String* TransformWide(const GuestLocale::CollateFacet* facet, GuestLocale::String* result, const std::u16string& text) { + return Vtable(&facet->base).transform(result, facet, Characters(text.data()), Characters(text.data() + text.size())); +} + +// The wide facet mirrors the narrow one in the facet it allocates and the C-locale validation, but it +// carries its own vtable, whose entries work on the guest's 16-bit characters. +void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const GuestLocale::CollateFacet* narrow) { + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); + + const std::size_t before = allocations; + GuestLocale::Facet existing{}; + GuestLocale::Facet* facet = &existing; + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet == &existing && allocations == before); + + GuestLocale::Implementation french = *_ZSt21_sceLibcClassicLocale_nid_postfix; + french.name = "fr_FR"; + const GuestLocale::Implementation* frenchPointer = &french; + GuestLocale::Implementation unnamed = *_ZSt21_sceLibcClassicLocale_nid_postfix; + unnamed.name = nullptr; + const GuestLocale::Implementation* unnamedPointer = &unnamed; + const GuestLocale::Implementation* missingPointer = nullptr; + for (const GuestLocale::Implementation* const* rejected : {&frenchPointer, &unnamedPointer, &missingPointer}) { + GuestLocale::Facet* absent = nullptr; + reject([&] { _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&absent, rejected); }); + require(absent == nullptr && allocations == before); + } + GuestLocale::Facet* absent = nullptr; + reject([&] { _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&absent, nullptr); }); + require(absent == nullptr && allocations == before); + + facet = nullptr; + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet != nullptr && allocations == before + 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); + const auto* wide = reinterpret_cast(facet); + require(facet->references == 0 && facet->vtable != narrow->base.vtable); + require(wide->collation == nullptr && wide->wideCollation == nullptr); + + const std::u16string text = u"abc"; + require(CompareWide(wide, text, u"abd") == -1); + require(CompareWide(wide, u"abd", text) == 1); + require(CompareWide(wide, text, text) == 0); + require(CompareWide(wide, u"ab", text) == -1); + require(CompareWide(wide, u"", text) == -1); + require(CompareWide(wide, u"", u"") == 0); + // Characters above 0xff order by their whole value: 0x0100 and 0x0001 compare the other way round + // when the same characters are read as the little-endian bytes 0x00 0x01 and 0x01 0x00. + require(CompareWide(wide, u"\u0100", u"\u0001") == 1); + require(CompareWide(wide, u"\u0001", u"\u0100") == -1); + require(CompareWide(wide, u"\u00ff", u"\u0100") == -1); + require(CompareWide(wide, u"\u00e9", u"z") == 1); + require(CompareWide(wide, u"a\u0101b", u"a\u0100b") == 1); + require(CompareWide(wide, u"a\u0101b", u"a\u0102b") == -1); + const std::u16string embeddedWide(u"a\0b", 3); + require(CompareWide(wide, embeddedWide, u"a") == 1); + require(CompareWide(wide, text, u"abd") == Compare(narrow, "abc", "abd")); + + require(HashWide(wide, u"") == 0xcbf29ce484222325ull); + require(HashWide(wide, text) == 0xe71fa2190541574bull); + require(HashWide(wide, u"a") == 0xaf63dc4c8601ec8cull); + // One round per character, so a wide string of characters below 0x100 hashes like its narrow + // spelling and a character above 0xff hashes like one value instead of the two bytes it holds. + require(HashWide(wide, u"foobar") == 0x85944171f73967e8ull); + require(HashWide(wide, u"foobar") == Hash(narrow, "foobar")); + require(HashWide(wide, u"\u0100") == 0x0af62bd4c860004dfull); + + GuestLocale::String result; + std::memset(&result, 0xcd, sizeof(result)); + require(TransformWide(wide, &result, text) == &result); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 3 && result.capacity == 7 && allocations == before + 1); + const auto* inlineUnits = reinterpret_cast(result.buffer); + require(inlineUnits[0] == u'a' && inlineUnits[1] == u'b' && inlineUnits[2] == u'c' && inlineUnits[3] == 0); + + std::memset(&result, 0xcd, sizeof(result)); + require(Vtable(facet).transform(&result, wide, nullptr, nullptr) == &result); + require(result.size == 0 && result.capacity == 7 && reinterpret_cast(result.buffer)[0] == 0); + + const std::u16string seven(7, u'q'); + require(TransformWide(wide, &result, seven) == &result); + require(result.size == 7 && result.capacity == 7 && allocations == before + 1); + const auto* inlineSeven = reinterpret_cast(result.buffer); + require(inlineSeven[6] == u'q' && inlineSeven[7] == 0); + + const std::u16string eight(8, u'y'); + require(TransformWide(wide, &result, eight) == &result); + require(allocations == before + 2 && lastSize == 32 && result.size == 8 && result.capacity == 15 && result.pointer == lastAllocation); + require(reinterpret_cast(result.pointer)[0] == u'y' && reinterpret_cast(result.pointer)[8] == 0); + std::free(result.pointer); + + for (const std::size_t length : {2039, 2040}) { + const std::u16string large(length, u'\u0100'); + require(TransformWide(wide, &result, large) == &result); + require(result.size == length && result.capacity == (length | 7)); + const auto* units = reinterpret_cast(result.pointer); + require(std::u16string(units, result.size) == large && units[result.size] == 0); + require(lastSize == (length == 2039 ? 4080 : 4096 + 0x27)); + if (length == 2040) { + require(reinterpret_cast(units) % 0x20 == 0); + require(reinterpret_cast(units)[-1] == lastAllocation); + } else require(result.pointer == lastAllocation); + std::free(lastAllocation); + } + + const std::u16string embeddedText(u"a\0b", 3); + reject([&] { TransformWide(wide, &result, embeddedText); }); + reject([&] { Vtable(facet).transform(nullptr, wide, Characters(seven.data()), Characters(seven.data() + seven.size())); }); + require(allocations == before + 4); + reject([&] { Vtable(facet).compare(wide, Characters(text.data() + 2), Characters(text.data()), Characters(text.data()), Characters(text.data() + 1)); }); + reject([&] { Vtable(facet).compare(wide, nullptr, Characters(text.data() + 1), Characters(text.data()), Characters(text.data() + 1)); }); + reject([&] { Vtable(facet).hash(wide, Characters(text.data() + 1), Characters(text.data())); }); + + Vtable(facet).facet.retain(facet); + require(facet->references == 1); + require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + const std::size_t freed = frees; + Vtable(facet).facet.destroy(facet); + require(frees == freed); + Vtable(facet).facet.deleteObject(facet); + require(frees == freed + 1 && lastFree == wide); + require(allocations == before + 4); +} + } int main() { @@ -155,9 +292,12 @@ int main() { require(facet->references == 2); require(Vtable(facet).facet.release(facet) == nullptr && facet->references == 1); require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + CheckWideGetcat(classic, collate); + Vtable(facet).facet.destroy(facet); - require(frees == 0); + require(frees == 1); Vtable(facet).facet.deleteObject(facet); - require(frees == 1 && lastFree == collate); + require(frees == 2 && lastFree == collate); + require(allocations == 8); return 0; } diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 5a9190e1ec..91c18ac93e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -16,6 +16,8 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_init_nid_postfix(PthreadCond* cond, const PthreadCondattr* attr); +int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); @@ -121,6 +123,15 @@ int main() { Require(scePthreadJoin(thread, nullptr) == SCE_OK); Require(scePthreadCondDestroy(&cond) == SCE_OK); + + PthreadCondattr unset = nullptr; + Require(scePthreadCondInit(&cond, &unset, nullptr) == SCE_OK); + Require(scePthreadMutexLock(&context.mutex) == SCE_OK); + Require(scePthreadCondTimedwait(&cond, &context.mutex, 1000) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(scePthreadMutexUnlock(&context.mutex) == SCE_OK); + Require(scePthreadCondDestroy(&cond) == SCE_OK); + Require(pthread_cond_init_nid_postfix(&cond, &unset) == 0); + Require(pthread_cond_destroy_nid_postfix(&cond) == 0); Require(scePthreadMutexDestroy(&context.mutex) == SCE_OK); Require(sceKernelSleep(0) == SCE_OK); diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp new file mode 100644 index 0000000000..8a79bb03db --- /dev/null +++ b/core/libs/tests/GuestContentDelete.cpp @@ -0,0 +1,49 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param); +extern "C" int APS5_VABI sceContentDeleteTerminate(void); +extern "C" int APS5_VABI sceContentDeleteById(std::int64_t contentId); +extern "C" int APS5_VABI sceContentDeleteByPath(const char* contentPath); + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +std::string LogicErrorMessage(TAction action) { + try { + action(); + } catch (const std::invalid_argument&) { + return "invalid argument"; + } catch (const std::logic_error& error) { + return error.what(); + } + return "returned"; +} + +bool Contains(const std::string& text, const std::string& part) { return text.find(part) != std::string::npos; } + +} + +int main() { + Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + + ContentDeleteInitParam param{}; + param.heap_size = 0x10000; + Require(sceContentDeleteInitialize(¶m) == 0); + + Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); + Require(Contains(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }), "sceContentDeleteById: content id 4886718345 is not in the host content library")); + Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); + + Require(sceContentDeleteTerminate() == 0); + Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + return 0; +} diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp deleted file mode 100644 index a70891bf8a..0000000000 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" - -#include -#include -#include - -extern "C" { -int APS5_VABI sceContentDeleteById(); - -} - -namespace { - -bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { - try { - function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", name, error.what()); - return false; - } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); - return false; -} - -} - -int main() { - bool correct = true; - correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - - return correct ? 0 : 1; -} diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index c25a9f31bd..7e4b8c1cc4 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -2,9 +2,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::uint32_t, char16_t*); } static void Require(bool value) { if (!value) std::abort(); } @@ -26,5 +28,18 @@ int main() { type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); Require(type == 0); + for (std::uint32_t state : {0u, 0x4000000u}) { + char16_t character[3] = {u'?', 0, 0}; + Require(sceConvertKeycodeGetCharacter(0x04, state, 0, character) == 0); + Require(character[0] == u'a'); + Require(sceConvertKeycodeGetCharacter(0x1E, state, 0, character) == 0); + Require(character[0] == u'1'); + Require(sceConvertKeycodeGetCharacter(0x3A, state, 0, character) == 0); + Require(character[0] == 0); + Require(sceConvertKeycodeGetCharacter(0x64, state, 0, character) == 0); + Require(character[0] == u'\\'); + Require(sceConvertKeycodeGetCharacter(0x32, state, 0, character) == 0); + Require(character[0] == 0); + } return 0; } diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index 5c12d2cddc..1fb6e65f1e 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -13,6 +13,7 @@ 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); +int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize); } namespace { @@ -40,11 +41,15 @@ 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; + alignas(uint64_t) std::array context{}; + for (size_t index = 0; index < context.size(); ++index) context[index] = static_cast(index * 5 + 1); + const auto originalContext = context; 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(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler); + Require(region == original && context == originalContext); Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); Require(sceCoredumpAttachMemoryRegion(0, region.data(), 1) == NotInCoredumpHandler); @@ -55,7 +60,10 @@ int main() { Require(fromThread == NotInCoredumpHandler); std::thread([&] { fromThread = sceCoredumpGetStopInfoCpu(region.data() + 16, 16); }).join(); Require(fromThread == NotInCoredumpHandler); - Require(region == original); + Require(sceCoredumpGetThreadContextInfo(context.data(), 144) == NotInCoredumpHandler); + std::thread([&] { fromThread = sceCoredumpGetThreadContextInfo(context.data() + 8, 144); }).join(); + Require(fromThread == NotInCoredumpHandler); + Require(region == original && context == originalContext); Require(!handlerRan); Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, nullptr, region.size()); }, "sceCoredumpAttachMemoryRegion")); @@ -69,12 +77,18 @@ int main() { 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(Throws([&] { sceCoredumpGetThreadContextInfo(nullptr, 144); }, "sceCoredumpGetThreadContextInfo")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data() + 1, 144); }, "sceCoredumpGetThreadContextInfo")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(reinterpret_cast(UINTPTR_MAX - 7u), 144); }, "sceCoredumpGetThreadContextInfo")); + for (const size_t size : {size_t{0}, size_t{1}, size_t{143}, size_t{145}, SIZE_MAX}) + Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data(), size); }, "sceCoredumpGetThreadContextInfo")); 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(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler); + Require(region == original && context == originalContext); Require(!handlerRan); return 0; } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index f6600788de..df087488e3 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -33,6 +33,7 @@ int APS5_VABI __fpclassifyd_nid_postfix(double value); unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); __int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor); unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); +__int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor); SonyLconv* APS5_VABI localeconv_nid_postfix(void); int APS5_VABI sceAmprFpclassifyd(double value); unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix(); @@ -110,6 +111,31 @@ int main() { } Require(trapped); + Require(__modti3_nid_postfix(100, 3) == 1); + Require(__modti3_nid_postfix(-100, 3) == -1); + Require(__modti3_nid_postfix(100, -3) == 1); + Require(__modti3_nid_postfix(-100, -3) == -1); + Require(SonyModti3(0, 5) == 0); + Require(SonyModti3(3, 100) == 3); + Require(SonyModti3(-3, 100) == -3); + Require(SonyModti3(3, -100) == 3); + Require(SonyModti3(-3, -100) == -3); + Require(SonyModti3(large, 3) == large % 3); + Require(SonyModti3(-large, 3) == -(large % 3)); + Require(SonyModti3(large, -3) == large % 3); + Require(SonyModti3(-large, -3) == -(large % 3)); + Require(SonyModti3(minimum, 1) == 0); + Require(SonyModti3(minimum, -1) == 0); + Require(SonyModti3(maximum, 1) == 0); + Require(SonyModti3(maximum, -1) == 0); + trapped = false; + try { + SonyModti3(1, 0); + } catch (const std::exception&) { + trapped = true; + } + Require(trapped); + const unsigned __int128 unsignedMaximum = ~(unsigned __int128)0; const unsigned __int128 largeDivisor = ((unsigned __int128)1 << 64) + 3; Require(__umodti3_nid_postfix(100, 3) == 1); diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 1eb0a5fb45..6ad61bd721 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -122,6 +123,16 @@ int main(int argc, char** argv) { void* hostAdd = ::dlsym(host, "3qcG+yBRS+s"); Require(hostAdd != nullptr); Require(dlsym_nid_postfix(nullptr, "GuestModuleAdd") == hostAdd); + void* hostMul = ::dlsym(host, "GuestModuleMul#guest"); + void* hostSub = ::dlsym(host, "BOyBJaKwOa8#guest"); + Require(hostMul != nullptr && hostSub != nullptr); + for (void* scope : std::array{nullptr, reinterpret_cast(-2)}) { + auto defaultMul = reinterpret_cast(dlsym_nid_postfix(scope, "GuestModuleMul")); + auto defaultSub = reinterpret_cast(dlsym_nid_postfix(scope, "GuestModuleSub")); + Require(reinterpret_cast(defaultMul) == hostMul); + Require(reinterpret_cast(defaultSub) == hostSub); + Require(defaultMul(6, 7) == 42 && defaultSub(50, 8) == 42); + } Require(dlsym_nid_postfix(nullptr, "malloc") == nullptr); Require(::dlclose(host) == 0); #endif diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index 7bd0c7fb88..46e2b2b11b 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -13,18 +13,25 @@ static void Require(bool value) { if (!value) std::abort(); } int main() { const char* key = "ANYPS5_GUEST_ENV_TEST_4C27"; #ifdef _WIN32 - Require(_putenv_s(key, "inherited") == 0); + Require(_putenv_s(key, "host") == 0); #else - Require(::setenv(key, "inherited", 1) == 0); + Require(::setenv(key, "host", 1) == 0); #endif - Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); + Require(getenv_nid_postfix(key) == nullptr); +#ifdef _WIN32 + Require(_putenv_s("HOME", "C:\\Users\\host") == 0); +#else + Require(::setenv("HOME", "/home/host", 1) == 0); +#endif + for (const char* host : {"HOME", "PATH"}) Require(std::getenv(host) != nullptr && getenv_nid_postfix(host) == nullptr); + Require(setenv_nid_postfix(key, "inherited", 0) == 0); Require(setenv_nid_postfix(key, "kept out", 0) == 0); Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); char value[] = "copied"; Require(setenv_nid_postfix(key, value, 1) == 0); value[0] = 'X'; Require(std::strcmp(getenv_nid_postfix(key), "copied") == 0); - Require(std::strcmp(std::getenv(key), "inherited") == 0); + Require(std::strcmp(std::getenv(key), "host") == 0); Require(setenv_nid_postfix(key, "", 1) == 0); Require(getenv_nid_postfix(key) && *getenv_nid_postfix(key) == 0); char borrowed[] = "ANYPS5_GUEST_ENV_TEST_4C27=one"; diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp index cb91dc4fbf..106819b6a8 100644 --- a/core/libs/tests/GuestErrorDialog.cpp +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -34,11 +34,16 @@ struct ErrorDialogParam { int main() { ErrorDialogParam param{16, static_cast(0x80020001), 0, 0}; + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogClose() == kErrInvalidState); Require(sceErrorDialogOpen(¶m) == kErrInvalidState); Require(sceErrorDialogInitialize() == 0); Require(sceErrorDialogInitialize() == kErrAlreadyInitialized); + Require(sceErrorDialogGetStatus() == 1); + Require(sceErrorDialogUpdateStatus() == 1); Require(sceErrorDialogClose() == kErrInvalidState); @@ -60,4 +65,7 @@ int main() { Require(sceErrorDialogTerminate() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); + Require(sceErrorDialogClose() == kErrInvalidState); } diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 76f392261f..f864d0e069 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include #include #include #include @@ -10,6 +12,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include #include @@ -30,6 +34,7 @@ struct GuestIovec { extern "C" { int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +int APS5_VABI dup_nid_postfix(int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); int APS5_VABI sceKernelLseek(int, std::int64_t, int); @@ -69,10 +74,111 @@ static std::string Contents(const std::filesystem::path& path) { return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } +static void ConcurrentRecords(const std::filesystem::path& path) { + constexpr std::size_t Workers = 4; + constexpr std::size_t Records = 256; + constexpr std::size_t Parts = 32; + constexpr std::size_t RecordBytes = Parts * sizeof(std::uint16_t); + { std::ofstream stream(path, std::ios::binary); Require(static_cast(stream)); } + std::array descriptors{}; + descriptors[0] = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(descriptors[0] >= 0); + for (std::size_t worker = 1; worker < Workers; ++worker) { + descriptors[worker] = dup_nid_postfix(descriptors[0]); + Require(descriptors[worker] >= 0); + } + std::barrier start(static_cast(Workers)); + std::array writers; + for (std::size_t worker = 0; worker < Workers; ++worker) { + writers[worker] = std::thread([&, worker] { + std::uint16_t label = 0; + std::array pieces; + pieces.fill({&label, sizeof(label)}); + start.arrive_and_wait(); + for (std::size_t record = 0; record < Records; ++record) { + label = static_cast(worker * Records + record); + const auto written = worker % 2 == 0 ? sceKernelWritev(descriptors[worker], pieces.data(), static_cast(pieces.size())) + : writev_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size())); + Require(written == RecordBytes); + } + }); + } + for (auto& writer : writers) writer.join(); + const auto bytes = Contents(path); + Require(bytes.size() == Workers * Records * RecordBytes); + std::array seen{}; + for (std::size_t offset = 0; offset < bytes.size(); offset += RecordBytes) { + std::array record; + std::memcpy(record.data(), bytes.data() + offset, RecordBytes); + Require(record[0] < seen.size()); + for (const auto label : record) { + if (label != record[0]) std::fprintf(stderr, "writev interleaved a record at byte %zu: %u versus %u\n", offset, static_cast(record[0]), static_cast(label)); + Require(label == record[0]); + } + ++seen[record[0]]; + } + for (const auto count : seen) Require(count == 1); + Require(sceKernelLseek(descriptors[0], 0, SEEK_SET) == 0); + std::array, Workers> labels; + std::array readers; + for (std::size_t worker = 0; worker < Workers; ++worker) { + readers[worker] = std::thread([&, worker] { + std::array record{}; + std::array pieces; + for (std::size_t part = 0; part < Parts; ++part) pieces[part] = {&record[part], sizeof(record[part])}; + start.arrive_and_wait(); + for (;;) { + const auto read = worker % 2 == 0 ? sceKernelReadv(descriptors[worker], pieces.data(), static_cast(pieces.size())) + : readv_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size())); + if (read == 0) break; + Require(read == RecordBytes); + for (const auto label : record) Require(label == record[0]); + labels[worker].push_back(record[0]); + } + }); + } + for (auto& reader : readers) reader.join(); + seen.fill(0); + for (const auto& records : labels) for (const auto label : records) { + Require(label < seen.size()); + ++seen[label]; + } + for (const auto count : seen) Require(count == 1); + Require(sceKernelLseek(descriptors[0], 0, SEEK_CUR) == static_cast(bytes.size())); +#ifdef _WIN32 + std::array positioned; + for (std::size_t worker = 0; worker < Workers; ++worker) { + positioned[worker] = std::thread([&, worker] { + std::array record{}; + std::array pieces; + for (std::size_t part = 0; part < Parts; ++part) pieces[part] = {&record[part], sizeof(record[part])}; + start.arrive_and_wait(); + for (std::size_t iteration = 0; iteration < Records; ++iteration) { + std::int64_t transferred; + if (worker % 2 == 0) { + record.fill(static_cast(0x8000 + worker * Records + iteration)); + transferred = worker == 0 ? sceKernelPwritev(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0) + : pwritev_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0); + } else { + transferred = worker == 1 ? sceKernelPreadv(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0) + : preadv_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0); + for (const auto label : record) Require(label == record[0]); + } + Require(transferred == RecordBytes); + } + }); + } + for (auto& transfer : positioned) transfer.join(); + Require(sceKernelLseek(descriptors[0], 0, SEEK_CUR) == static_cast(bytes.size())); +#endif + for (const auto descriptor : descriptors) Require(sceKernelClose(descriptor) == 0); +} + int main() { const auto root = std::filesystem::path("anyps5-file-vector-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); + ConcurrentRecords(root / "concurrent.bin"); const auto path = root / "data.bin"; { std::ofstream stream(path, std::ios::binary); stream << "0123456789"; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..f14e191c80 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -249,6 +249,55 @@ static void TestRelinkedModuleNameCase() { std::filesystem::remove_all(root); } +#ifdef _WIN32 +static std::filesystem::path ExtendedPath(const std::filesystem::path& path) { + const auto& native = path.native(); + if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return path; + return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) + : std::filesystem::path(L"\\\\?\\" + native); +} + +static void TestLongHostPathStat() { + auto root = std::filesystem::absolute(std::filesystem::temp_directory_path()).lexically_normal(); + if (root.filename().empty()) root = root.parent_path(); + root /= L"anyps5-stat-long-" + std::to_wstring(std::chrono::steady_clock::now().time_since_epoch().count()); + auto deep = root; + while (deep.native().size() < MAX_PATH + 24) deep /= std::wstring(40, L'd'); + Require(::CreateDirectoryW(ExtendedPath(root).c_str(), nullptr) != 0); + auto directory = root; + for (const auto& component : std::filesystem::path(deep).lexically_relative(root)) { + directory /= component; + Require(::CreateDirectoryW(ExtendedPath(directory).c_str(), nullptr) != 0); + } + const auto file = deep / L"payload.bin"; + const auto handle = ::CreateFileW(ExtendedPath(file).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + DWORD written = 0; + const bool wrote = ::WriteFile(handle, "metadata", 8, &written, nullptr) != 0 && written == 8; + ::CloseHandle(handle); + Require(wrote); + const auto host = deep.string(); + AddPathAlias_nid_no_patch("/long-stat-host", host.c_str()); + FileStat status{}; + Require(ResolvePath_nid_no_patch("/long-stat-host/payload.bin").native().size() > MAX_PATH); + Require(sceKernelStat("/long-stat-host/payload.bin", &status) == 0 && status.st_size == 8); + Require(sceKernelStat("/long-stat-host/missing.bin", &status) == static_cast(0x80020002u)); + Require(sceKernelStat("/long-stat-host/", &status) == 0 && (status.st_mode & 0170000) == 0040000); + RemovePathAlias_nid_no_patch("/long-stat-host"); + const auto extendedHost = ExtendedPath(deep).string(); + AddPathAlias_nid_no_patch("/long-stat-extended", extendedHost.c_str()); + Require(ResolvePath_nid_no_patch("/long-stat-extended/payload.bin").native().starts_with(L"\\\\?\\")); + Require(sceKernelStat("/long-stat-extended/payload.bin", &status) == 0 && status.st_size == 8); + Require(sceKernelStat("/long-stat-extended/missing.bin", &status) == static_cast(0x80020002u)); + RemovePathAlias_nid_no_patch("/long-stat-extended"); + ::DeleteFileW(ExtendedPath(file).c_str()); + for (auto path = deep; ; path = path.parent_path()) { + ::RemoveDirectoryW(ExtendedPath(path).c_str()); + if (path == root) break; + } +} +#endif + #ifdef __linux__ static int DirectoryOpens(int watch) { alignas(inotify_event) char buffer[4096]; @@ -290,6 +339,9 @@ int main() { TestParentTraversal(); TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); +#ifdef _WIN32 + TestLongHostPathStat(); +#endif #ifdef __linux__ TestMissingPathListsDirectoryOnce(); #endif @@ -304,6 +356,7 @@ int main() { std::uint64_t throttling[4] = {1, 2, 3, 4}; Require(sceKernelWriteThrottlingStatus(throttling) == 0); Require(throttling[0] == 0xffffffffu && throttling[1] == 0 && throttling[2] == 0 && throttling[3] == 0); + Require(sceKernelWriteThrottlingStatus(nullptr) == static_cast(0x8002000eu)); Require(sceKernelDebugOutText(0, nullptr) == static_cast(0x8002000eu)); auto* captured = std::tmpfile(); Require(captured != nullptr); diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 66ab937910..276459ec07 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -112,6 +112,9 @@ int main() { stream = Open(file, In | Noreplace); Require(stream != nullptr); fclose_nid_postfix(stream); + stream = Open(file, In | Binary | Noreplace); + Require(stream != nullptr); + fclose_nid_postfix(stream); Require(Open(missing, Out | Nocreate) == nullptr); Require(!Exists(missing)); diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp index 75dd90152b..742fad398f 100644 --- a/core/libs/tests/GuestFminf.cpp +++ b/core/libs/tests/GuestFminf.cpp @@ -5,42 +5,68 @@ #include extern "C" { +double APS5_VABI fmin_nid_postfix(double, double); float APS5_VABI fminf_nid_postfix(float, float); +long double APS5_VABI fminl_nid_postfix(long double, long double); +double APS5_VABI fmax_nid_postfix(double, double); +float APS5_VABI fmaxf_nid_postfix(float, float); +long double APS5_VABI fmaxl_nid_postfix(long double, long double); } -static void Check(bool condition, int line) { +static void Check(bool condition, const char* name, int line) { if (!condition) { - std::fprintf(stderr, "fminf check failed at line %d\n", line); + std::fprintf(stderr, "%s check failed at line %d\n", name, line); std::abort(); } } -#define Require(value) Check((value), __LINE__) +#define Require(value) Check((value), name, __LINE__) + +template +static void CheckMinAndMax(T (APS5_VABI *minimum)(T, T), T (APS5_VABI *maximum)(T, T), const char* name) { + const auto nan = std::numeric_limits::quiet_NaN(); + const auto infinity = std::numeric_limits::infinity(); + + Require(minimum(T(3.0), T(-2.0)) == T(-2.0)); + Require(minimum(T(-2.0), T(3.0)) == T(-2.0)); + Require(minimum(T(1.5), T(2.5)) == T(1.5)); + Require(maximum(T(3.0), T(-2.0)) == T(3.0)); + Require(maximum(T(-2.0), T(3.0)) == T(3.0)); + Require(maximum(T(1.5), T(2.5)) == T(2.5)); + + Require(minimum(nan, T(4.0)) == T(4.0)); + Require(minimum(T(4.0), nan) == T(4.0)); + Require(std::isnan(minimum(nan, nan))); + Require(maximum(nan, T(4.0)) == T(4.0)); + Require(maximum(T(4.0), nan) == T(4.0)); + Require(std::isnan(maximum(nan, nan))); + + Require(std::signbit(minimum(T(-0.0), T(+0.0)))); + Require(std::signbit(minimum(T(+0.0), T(-0.0)))); + Require(std::signbit(minimum(T(-0.0), T(-0.0)))); + Require(!std::signbit(minimum(T(+0.0), T(+0.0)))); + Require(!std::signbit(maximum(T(-0.0), T(+0.0)))); + Require(!std::signbit(maximum(T(+0.0), T(-0.0)))); + Require(std::signbit(maximum(T(-0.0), T(-0.0)))); + Require(!std::signbit(maximum(T(+0.0), T(+0.0)))); + + Require(minimum(infinity, -infinity) == -infinity); + Require(minimum(-infinity, infinity) == -infinity); + Require(minimum(infinity, T(5.0)) == T(5.0)); + Require(minimum(T(5.0), infinity) == T(5.0)); + Require(minimum(-infinity, T(5.0)) == -infinity); + Require(minimum(T(5.0), -infinity) == -infinity); + Require(maximum(infinity, -infinity) == infinity); + Require(maximum(-infinity, infinity) == infinity); + Require(maximum(infinity, T(5.0)) == infinity); + Require(maximum(T(5.0), infinity) == infinity); + Require(maximum(-infinity, T(5.0)) == T(5.0)); + Require(maximum(T(5.0), -infinity) == T(5.0)); +} 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); + CheckMinAndMax(fmin_nid_postfix, fmax_nid_postfix, "fmin/fmax"); + CheckMinAndMax(fminf_nid_postfix, fmaxf_nid_postfix, "fminf/fmaxf"); + CheckMinAndMax(fminl_nid_postfix, fmaxl_nid_postfix, "fminl/fmaxl"); return 0; } diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index fd90eb3349..53f9158dd4 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -56,6 +58,9 @@ FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter*, int*); +FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(const FontTextCharacter*, const FontTextCharacter*, FontTextCodes*); +FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes*); +FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes*); } static void Check(bool value, int line) { @@ -153,6 +158,60 @@ static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, Fo return 1; } +static void CheckTextCodes(const FontMemory* memory, FontHandle font) { + ParsedText parsed{"AVA", 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, 3, ParseText, &parsed) == SCE_FONT_OK); + FontString string = nullptr; + Require(sceFontCreateString(memory, &source, nullptr, &string) == SCE_FONT_OK && string != nullptr); + std::uint32_t count = 0; + FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &count); + Require(characters != nullptr && count == 3); + + FontTextCodes codes; + std::memset(&codes, 0xa5, sizeof(codes)); + Require(sceFontCharactersRefersTextCodes(characters, nullptr, &codes) == &codes); + Require(codes.text.order == parsed.text && codes.text.code == 'A' && codes.text.reserved == 0); + Require(codes.systemUse[5] == characters && codes.systemUse[6] == characters && codes.systemUse[7] == nullptr); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.code == 'V' && codes.text.order == parsed.text + 1); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.code == 'A' && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepNext(&codes) == nullptr && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.code == 'V' && codes.text.order == parsed.text + 1); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.code == 'A' && codes.text.order == parsed.text); + Require(sceFontTextCodesStepBack(&codes) == nullptr && codes.text.order == parsed.text); + + Require(sceFontCharactersRefersTextCodes(characters + 1, characters + 2, &codes) == &codes && codes.text.code == 'V'); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepNext(&codes) == nullptr && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.order == parsed.text + 1); + Require(sceFontTextCodesStepBack(&codes) == nullptr && codes.text.order == parsed.text + 1); + + Require(sceFontCharactersRefersTextCodes(characters + 2, characters + 2, &codes) == &codes && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepNext(&codes) == nullptr && sceFontTextCodesStepBack(&codes) == nullptr); + + characters[1].synthetic = 1; + Require(sceFontCharactersRefersTextCodes(characters, nullptr, &codes) == &codes); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.order == parsed.text); + Require(sceFontCharactersRefersTextCodes(characters + 1, nullptr, &codes) == nullptr && codes.text.order == parsed.text); + characters[1].synthetic = 0; + characters[1].clusterIndex = 1; + Require(sceFontCharactersRefersTextCodes(characters + 1, nullptr, &codes) == nullptr && codes.text.order == parsed.text); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.order == parsed.text + 1); + characters[1].clusterIndex = 0; + + Require(sceFontCharactersRefersTextCodes(characters, nullptr, nullptr) == nullptr); + Require(sceFontTextCodesStepNext(nullptr) == nullptr && sceFontTextCodesStepBack(nullptr) == nullptr); + bool threw = false; + try { + sceFontCharactersRefersTextCodes(nullptr, nullptr, &codes); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && codes.text.order == parsed.text + 1); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); +} + static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } @@ -363,6 +422,7 @@ int main() { Require(out.letter->x == step->x && out.letter->Components.textsIndex == 1 && out.letter->Components.glyphsIndex == 1); Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == characters + 1); Require(sceFontDestroyString(&string) == SCE_FONT_OK); + CheckTextCodes(&memory, font); Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK && renderer == nullptr); diff --git a/core/libs/tests/GuestFopenErrors.cpp b/core/libs/tests/GuestFopenErrors.cpp new file mode 100644 index 0000000000..5c72df7df0 --- /dev/null +++ b/core/libs/tests/GuestFopenErrors.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); +int APS5_VABI fclose_nid_postfix(void* stream); +void* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, void* stream); +int APS5_VABI fopen_s_nid_postfix(void** result, const char* filename, const char* mode); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fopen error check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GuestEnoent = 2; +constexpr int GuestEisdir = 21; +constexpr int GuestEinval = 22; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; + +const std::string LongName(300, 'n'); + +std::filesystem::path EnterFreshRoot() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fopen-errors-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root / "folder"); + std::filesystem::current_path(root); + Check(::symlink("loop", "loop") == 0); + { std::ofstream file("plain.txt"); file << "plain"; } + return root; +} + +bool OpenFails(const char* name, const char* mode, int error) { + *__error_nid_postfix() = 0; + const bool failed = fopen_nid_postfix(name, mode) == nullptr && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +void Open() { + Check(OpenFails("loop", "r", GuestEloop)); + Check(OpenFails("loop", "w", GuestEloop)); + Check(OpenFails("loop/inside", "r", GuestEloop)); + Check(OpenFails(LongName.c_str(), "r", GuestEnametoolong)); + Check(OpenFails(LongName.c_str(), "w", GuestEnametoolong)); + Check(OpenFails((LongName + "/inside").c_str(), "a", GuestEnametoolong)); + Check(OpenFails("missing.txt", "r", GuestEnoent)); + Check(OpenFails("folder", "w", GuestEisdir)); + void* plain = fopen_nid_postfix("plain.txt", "r"); + Check(plain != nullptr && fclose_nid_postfix(plain) == 0); +} + +bool ReopenFails(const char* name, const char* mode, int error) { + void* stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + *__error_nid_postfix() = 0; + const bool failed = freopen_nid_postfix(name, mode, stream) == nullptr && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +void Reopen() { + Check(ReopenFails("loop", "r", GuestEloop)); + Check(ReopenFails("loop/inside", "w", GuestEloop)); + Check(ReopenFails(LongName.c_str(), "r", GuestEnametoolong)); + Check(ReopenFails(LongName.c_str(), "w", GuestEnametoolong)); + Check(ReopenFails("missing.txt", "r", GuestEnoent)); + void* stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + void* reopened = freopen_nid_postfix("plain.txt", "r", stream); + Check(reopened != nullptr && fclose_nid_postfix(reopened) == 0); +} + +void CheckedOpen() { + void* stream = reinterpret_cast(1); + Check(fopen_s_nid_postfix(&stream, "loop", "r") == GuestEloop && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, LongName.c_str(), "w") == GuestEnametoolong && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "folder", "w") == GuestEisdir && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "plain.txt", "z") == GuestEinval && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "missing.txt", "r") == GuestEnoent && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "plain.txt", "r") == 0 && stream != nullptr && fclose_nid_postfix(stream) == 0); +} + +} + +int main() { + const auto root = EnterFreshRoot(); + Open(); + Reopen(); + CheckedOpen(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestFopenLongHostPath.cpp b/core/libs/tests/GuestFopenLongHostPath.cpp new file mode 100644 index 0000000000..9ebe1738ce --- /dev/null +++ b/core/libs/tests/GuestFopenLongHostPath.cpp @@ -0,0 +1,128 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#define NOMINMAX +#include + +extern "C" { +void* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(void*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, void*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +std::filesystem::path Extended(const std::filesystem::path& path) { + const auto& native = path.native(); + return native.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + native.substr(2) : L"\\\\?\\" + native; +} + +using Stream = std::unique_ptr; + +Stream Open(const char* path, const char* mode) { + Stream stream(fopen_nid_postfix(path, mode), fclose_nid_postfix); + if (!stream) { + std::fprintf(stderr, "fopen mode %s failed: guest errno=%d, resolved characters=%zu\n", mode, *__error_nid_postfix(), ResolvePath_nid_no_patch(path).native().size()); + throw std::runtime_error("fopen rejected a valid mapped long host path"); + } + return stream; +} + +void Run() { + auto temp = std::filesystem::absolute(std::filesystem::temp_directory_path()).lexically_normal(); + if (temp.filename().empty()) temp = temp.parent_path(); + const auto root = temp / ("anyps5-fopen-long-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(root.parent_path() == temp && root.filename().string().starts_with("anyps5-fopen-long-"), "invalid fixture root"); + Require(!std::filesystem::exists(root), "fixture root already exists"); + auto deep = root; + while (deep.native().size() < 290) deep /= std::string(40, 'd'); + Require(deep.native().starts_with(root.native() + L"\\"), "fixture escapes its root"); + struct Cleanup { + std::filesystem::path root; + std::filesystem::path deep; + ~Cleanup() { + RemovePathAlias_nid_no_patch("/long-host-file"); + RemovePathAlias_nid_no_patch("/extended-host-file"); + RemovePathAlias_nid_no_patch("/relative-long-host-file"); + DeleteFileW(Extended(deep / "payload.bin").c_str()); + DeleteFileW(Extended(deep / "new.bin").c_str()); + for (auto path = deep; ; path = path.parent_path()) { + RemoveDirectoryW(Extended(path).c_str()); + if (path == root) break; + } + } + } cleanup{root, deep}; + Require(std::filesystem::create_directories(Extended(deep)), "cannot create fixture directories"); + const auto payload = deep / "payload.bin"; + const auto handle = CreateFileW(Extended(payload).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(handle != INVALID_HANDLE_VALUE, "cannot prepare long-path file"); + DWORD written = 0; + const bool prepared = WriteFile(handle, "abcd", 4, &written, nullptr) && written == 4; + CloseHandle(handle); + Require(prepared, "cannot write fixture payload"); + const auto host = deep.string(); + AddPathAlias_nid_no_patch("/long-host-file", host.c_str()); + AddPathAlias_nid_no_patch("/extended-host-file", Extended(deep).string().c_str()); + Require(ResolvePath_nid_no_patch("/long-host-file/payload.bin").native().size() > MAX_PATH, "fixture does not cross MAX_PATH"); + for (const auto* path : {"/long-host-file/payload.bin", "/extended-host-file/payload.bin"}) { + auto stream = Open(path, "rb"); + std::array data{}; + Require(fread_nid_postfix(data.data(), 1, data.size(), stream.get()) == 4 && data == std::array{'a', 'b', 'c', 'd'}, "long-path contents differ"); + } + { + auto stream = Open("/long-host-file/new.bin", "wbx"); + Require(fwrite_nid_postfix("new", 1, 3, stream.get()) == 3, "long-path exclusive write failed"); + } + *__error_nid_postfix() = 0; + Require(!fopen_nid_postfix("/long-host-file/new.bin", "wbx") && *__error_nid_postfix() == 17, "existing long-path exclusive open succeeded"); + { + auto stream = Open("/long-host-file/new.bin", "ab"); + Require(fwrite_nid_postfix("!", 1, 1, stream.get()) == 1, "long-path append failed"); + } + { + auto stream = Open("/long-host-file/new.bin", "rb+"); + std::array data{}; + Require(fread_nid_postfix(data.data(), 2, 2, stream.get()) == 2 && data == std::array{'n', 'e', 'w', '!'}, "long-path append bytes differ"); + } + *__error_nid_postfix() = 0; + Require(!fopen_nid_postfix("/long-host-file/missing.bin", "rb") && *__error_nid_postfix() == 2, "missing long-path file did not report ENOENT"); + const auto originalDirectory = std::filesystem::current_path(); + { + struct RestoreDirectory { + std::filesystem::path original; + ~RestoreDirectory() { + std::error_code error; + std::filesystem::current_path(original, error); + } + } restoreDirectory{originalDirectory}; + std::filesystem::current_path(root); + const auto relative = deep.lexically_relative(root).string(); + Require(!relative.empty() && !std::filesystem::path(relative).is_absolute(), "fixture alias is not relative"); + AddPathAlias_nid_no_patch("/relative-long-host-file", relative.c_str()); + auto stream = Open("/relative-long-host-file/payload.bin", "rb"); + std::array data{}; + Require(fread_nid_postfix(data.data(), 1, data.size(), stream.get()) == 4 && data == std::array{'a', 'b', 'c', 'd'}, "relative long-path contents differ"); + } + Require(std::filesystem::current_path() == originalDirectory, "fixture did not restore working directory"); + std::puts("long host path read/create/exclusive/append/update/missing/relative checks passed"); +} + +} + +int main() { + try { Run(); } + catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; } + return 0; +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index e74dc2cd38..40972ab464 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -79,6 +79,21 @@ static void UnsupportedResolutionStillThrows() { Require(sceKernelClockGetres(3, nullptr) == fault); } +static void OmittedTimeIsFaultAfterClockValidation() { + for (const int clockId : {GUEST_CLOCK_REALTIME, 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}) { + *__error_nid_postfix() = 0; + Require(clock_gettime_nid_postfix(clockId, nullptr) == -1 && *__error_nid_postfix() == 14); + } + bool rejected = false; + try { + clock_gettime_nid_postfix(3, nullptr); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} + static KernelTimespec Read(int clockId) { KernelTimespec time{-1, -1}; Require(clock_gettime_nid_postfix(clockId, &time) == SCE_OK); @@ -297,6 +312,7 @@ static void CalendarConversionsFollowTheHostZone() { int main() { if (!OmittedResolutionSucceeds()) return 1; UnsupportedResolutionStillThrows(); + OmittedTimeIsFaultAfterClockValidation(); SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); CalendarConversionsFollowTheHostZone(); diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp new file mode 100644 index 0000000000..4f0aa66bb9 --- /dev/null +++ b/core/libs/tests/GuestFstatFault.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstat check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; + +static void RequireFailure(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(descriptor, nullptr) == -1); + Require(*__error_nid_postfix() == error); + Require(sceKernelFstat(descriptor, nullptr) == static_cast(0x80020000u | static_cast(error))); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fstat-fault-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { + std::ofstream file(root / "file.txt", std::ios::binary); + file << "fstat"; + Require(static_cast(file)); + } + + AddPathAlias_nid_no_patch("/anyps5-fstat-fault-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstat-fault-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(file, &status) == 0 && status.st_size == 5); + RequireFailure(file, GuestEfault); + + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + RequireFailure(sockets[0], GuestEfault); + Require(close_nid_postfix(sockets[0]) == 0); + Require(close_nid_postfix(sockets[1]) == 0); + Require(close_nid_postfix(file) == 0); +#ifndef _WIN32 + RequireFailure(file, GuestEbadf); + RequireFailure(-1, GuestEbadf); +#endif + + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index d1666fd28d..e896a87e42 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -16,6 +16,8 @@ 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); @@ -31,10 +33,15 @@ int main() { Require(sceG2PDialogTerminate() == kErrNotInitialized); Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); Require(sceG2PDialogGetResult(result) == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); Require(sceG2PDialogInitialize() == 0); Require(sceG2PDialogInitialize() == kErrAlreadyInitialized); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + Require(sceG2PDialogUpdateStatus() == kStatusInitialized); Require(sceG2PDialogOpen(nullptr) == kErrArgNull); Require(sceG2PDialogGetResult(result) == kErrNotFinished); @@ -51,4 +58,5 @@ int main() { Require(sceG2PDialogClose() == 0); Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 12aa20e597..0394f1283d 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -27,10 +27,7 @@ int main() { Require(sceGameUpdateInitialize() == 0); Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); - bool threw = false; - try { sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); info.size = sizeof(info) - 1; Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == invalidSize); diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp index 57e484e580..04f037f499 100644 --- a/core/libs/tests/GuestGetitimer.cpp +++ b/core/libs/tests/GuestGetitimer.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include struct GuestItimerval { @@ -62,16 +61,14 @@ int main() { Require(getitimer_nid_postfix(invalid, nullptr) == -1 && *__error_nid_postfix() == 22, "invalid timer validation order changed"); } Require(getitimer_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == 14, "null output did not report EFAULT"); - for (int unsupported : {1, 2}) { + for (int disarmed : {1, 2}) { GuestItimerval output{{11, 22}, {33, 44}}; - const auto saved = output; - bool threw = false; - try { - getitimer_nid_postfix(unsupported, &output); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw && std::memcmp(&output, &saved, sizeof(output)) == 0, "unsupported CPU timer reported fabricated state"); + *__error_nid_postfix() = 12345; + Require(getitimer_nid_postfix(disarmed, &output) == 0, "CPU timer read failed"); + Require(*__error_nid_postfix() == 12345, "CPU timer read changed errno"); + Require(output.interval.tv_sec == 0 && output.interval.tv_usec == 0 && output.value.tv_sec == 0 && output.value.tv_usec == 0, + "a CPU timer that cannot be armed did not read as disarmed"); + Require(getitimer_nid_postfix(disarmed, nullptr) == -1 && *__error_nid_postfix() == 14, "null CPU timer output did not report EFAULT"); } const auto ignore = reinterpret_cast(std::uintptr_t{1}); diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index b02f16a287..2f57340267 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -75,6 +75,25 @@ static void CheckOpenFileLimit() { Require(host.rlim_cur == RLIM_INFINITY ? limit.rlim_cur == GUEST_RLIM_INFINITY : limit.rlim_cur == static_cast(host.rlim_cur)); #endif Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &limit) == 0); + const ResourceLimit lowered{limit.rlim_max > 1 ? limit.rlim_max - 1 : 1, limit.rlim_max}; + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &lowered) == 0); + const ResourceLimit updated = GuestLimit(GUEST_RLIMIT_NOFILE); + Require(updated.rlim_cur == lowered.rlim_cur); + Require(updated.rlim_max == lowered.rlim_max); + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &limit) == 0); + const ResourceLimit restored = GuestLimit(GUEST_RLIMIT_NOFILE); + Require(restored.rlim_cur == limit.rlim_cur); + Require(restored.rlim_max == limit.rlim_max); + if (limit.rlim_max != GUEST_RLIM_INFINITY) { + const ResourceLimit raised{limit.rlim_cur, limit.rlim_max + 1}; + bool raisedRejected = false; + try { + setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &raised); + } catch (const std::runtime_error&) { + raisedRejected = true; + } + Require(raisedRejected); + } } static void RequireError(int resource, ResourceLimit* limit, int error) { diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp index da998f3e4b..7fefbdca94 100644 --- a/core/libs/tests/GuestGetrusage.cpp +++ b/core/libs/tests/GuestGetrusage.cpp @@ -14,6 +14,7 @@ struct ResourceUsage { std::int64_t rest[14]; }; extern "C" int APS5_VABI getrusage_nid_postfix(int who, ResourceUsage* usage); +extern "C" int* APS5_VABI __error_nid_postfix(); static void Require(bool value) { if (!value) std::abort(); } static std::int64_t CpuMicros(int who) { ResourceUsage usage{}; @@ -28,7 +29,22 @@ static void SpinUntil(int who, std::int64_t micros) { Require(std::chrono::steady_clock::now() < deadline); } } +static void RequireFailure(int who, ResourceUsage* usage, int error) { + *__error_nid_postfix() = 0; + Require(getrusage_nid_postfix(who, usage) == -1); + Require(*__error_nid_postfix() == error); +} int main() { + ResourceUsage untouched; + std::memset(&untouched, 0x5a, sizeof(untouched)); + const ResourceUsage pattern = untouched; + RequireFailure(2, &untouched, 22); + RequireFailure(-2, &untouched, 22); + Require(std::memcmp(&untouched, &pattern, sizeof(untouched)) == 0); + RequireFailure(7, nullptr, 22); + RequireFailure(0, nullptr, 14); + RequireFailure(1, nullptr, 14); + RequireFailure(-1, nullptr, 14); ResourceUsage children; std::memset(&children, 0xff, sizeof(children)); Require(getrusage_nid_postfix(-1, &children) == 0); diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index 72e2ea7025..bf4df157ce 100644 --- a/core/libs/tests/GuestHostStandardError.cpp +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -11,6 +11,9 @@ #include #include #include +#ifdef __linux__ +#include +#endif extern "C" { FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); @@ -25,6 +28,12 @@ extern FileStream* __stderrp_nid_postfix; static void Require(bool value) { if (!value) std::abort(); } +static void SkipCrashHandler() { +#ifdef __linux__ + Require(::prctl(PR_SET_DUMPABLE, 0) == 0); +#endif +} + enum class Redirect { Duplicate, Reopen }; struct Outcome { @@ -56,7 +65,10 @@ static Outcome Run(Redirect redirect, const std::filesystem::path& probe, bool t ::close(host[1]); RedirectGuest(redirect, probe); std::fputs("host\n", stderr); - if (terminate) throw std::runtime_error("report kept"); + if (terminate) { + SkipCrashHandler(); + throw std::runtime_error("report kept"); + } if (redirect == Redirect::Reopen && fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(71); ::_exit(0); } diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index aa184acce1..e4555bd5cc 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include #include +#include #include extern "C" { @@ -45,6 +47,27 @@ static void Require(bool value) { if (!value) std::abort(); } static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } +static void CheckUriEscapeBytes() { + constexpr char unreserved[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + constexpr char hex[] = "0123456789ABCDEF"; + for (unsigned value = 1; value <= 255; ++value) { + const char input[] = {static_cast(value), '\0'}; + char expected[] = {'%', hex[value >> 4], hex[value & 15], '\0'}; + if (std::strchr(unreserved, input[0])) { + expected[0] = input[0]; + expected[1] = '\0'; + } + const auto size = std::strlen(expected) + 1; + std::size_t required = 0; + Require(sceHttpUriEscape(nullptr, &required, 0, input) == 0 && required == size); + char output[5] = {'Z', 'Z', 'Z', 'Z', 'Z'}; + Require(sceHttpUriEscape(output, &required, size - 1, input) == static_cast(0x80431022)); + for (char byte : output) Require(byte == 'Z'); + Require(sceHttpUriEscape(output, &required, size, input) == 0 && required == size); + Require(Equal(output, expected) && output[size] == 'Z'); + } +} + static int AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } static void APS5_VABI RequestStatusCallback(int, int, void* argument) { ++*static_cast(argument); @@ -137,6 +160,13 @@ int main() { Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), "a b/~\xC3\xA9") == 0); Require(Equal(escaped, "a%20b%2F~%C3%A9")); Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), nullptr) == invalidValue); + CheckUriEscapeBytes(); +#ifdef _WIN32 + const std::string previousCtype = std::setlocale(LC_CTYPE, nullptr); + Require(std::setlocale(LC_CTYPE, ".1252") != nullptr); + CheckUriEscapeBytes(); + Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); +#endif constexpr const char* unescapeCases[][2] = { {"", ""}, {"plain+text", "plain+text"}, {"a%20b%2F~%c3%a9", "a b/~\xC3\xA9"}, diff --git a/core/libs/tests/GuestImeKeyboard.cpp b/core/libs/tests/GuestImeKeyboard.cpp index cd3dbaf247..180a47106f 100644 --- a/core/libs/tests/GuestImeKeyboard.cpp +++ b/core/libs/tests/GuestImeKeyboard.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode); static_assert(sizeof(KeyboardInfo) == 36); +constexpr int AlreadyOpened = static_cast(0x80bc0001u); constexpr int NotOpened = static_cast(0x80bc0002u); constexpr int InvalidUserId = static_cast(0x80bc0010u); constexpr int NoResourceId = static_cast(0x80bc0023u); @@ -35,6 +36,7 @@ static void CheckClosed() { Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info"); Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info without an open keyboard"); RequireUntouched(info); + Require(sceImeKeyboardClose(1) == NotOpened, "close without an open keyboard"); Require(sceImeKeyboardSetMode(-1, 0) == InvalidUserId, "set mode invalid user"); Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode without an open keyboard"); Require(sceImeKeyboardSetMode(1, 0x80) == NotOpened, "set mode checks the mode before the keyboard"); @@ -43,6 +45,7 @@ static void CheckClosed() { static void CheckOpened() { KeyboardParam param{}; Require(sceImeKeyboardOpen(1, ¶m) == 0, "keyboard open failed"); + Require(sceImeKeyboardOpen(1, ¶m) == AlreadyOpened, "duplicate open accepted"); KeyboardInfo info; std::memset(&info, 0xa5, sizeof(info)); Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info with an open keyboard"); @@ -57,6 +60,7 @@ static void CheckOpened() { Require(sceImeKeyboardSetMode(1, 0x80) == InvalidMode, "mode bit 7 accepted"); Require(sceImeKeyboardSetMode(1, 0x80000001u) == InvalidMode, "mode bit 31 accepted"); Require(sceImeKeyboardClose(1) == 0, "keyboard close failed"); + Require(sceImeKeyboardClose(1) == NotOpened, "second close accepted"); Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info after close"); Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode after close"); } diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index eeb24342a3..4a7202cf4e 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -7,7 +7,9 @@ #include #include #endif +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -33,6 +35,7 @@ static_assert(offsetof(GuestInterfaceAddress, data) == 48); extern "C" { int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list); void APS5_VABI freeifaddrs_nid_postfix(GuestInterfaceAddress* list); +void APS5_VABI free_nid_postfix(void* pointer); int* APS5_VABI __error_nid_postfix(); } @@ -106,6 +109,8 @@ static std::vector HostSnapshot() { } int main() { + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); constexpr std::uint32_t knownFlags = 0x1 | 0x2 | 0x8 | 0x10 | 0x40 | 0x80 | 0x100 | 0x200 | 0x8000; const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; @@ -167,6 +172,9 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); + GuestInterfaceAddress* released = nullptr; + Require(getifaddrs_nid_postfix(&released) == 0 && released != nullptr); + free_nid_postfix(released); freeifaddrs_nid_postfix(nullptr); *__error_nid_postfix() = 0; Require(getifaddrs_nid_postfix(nullptr) == -1); diff --git a/core/libs/tests/GuestKernelAppArchive.cpp b/core/libs/tests/GuestKernelAppArchive.cpp new file mode 100644 index 0000000000..2eb8aae01f --- /dev/null +++ b/core/libs/tests/GuestKernelAppArchive.cpp @@ -0,0 +1,257 @@ +#include "AppArchiveFixture.hpp" +#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 +#include + +struct KernelIovec { + void* base; + std::size_t length; +}; + +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 sceKernelWrite(int d, const 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 sceKernelReadv(int d, const KernelIovec* iov, int iovcnt); +std::int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, std::int64_t offset); +std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); +int APS5_VABI sceKernelStat(const char* path, FileStat* sb); +int APS5_VABI sceKernelFstat(int d, FileStat* sb); +int APS5_VABI sceKernelCheckReachability(const char* path); +int APS5_VABI dup_nid_postfix(int d); +int APS5_VABI dup2_nid_postfix(int from, int to); +int APS5_VABI close_nid_postfix(int d); +std::int64_t APS5_VABI pread_nid_postfix(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI lseek_nid_postfix(int d, std::int64_t offset, int whence); +int APS5_VABI fcntl_nid_postfix(int d, int command, ...); +int APS5_VABI ioctl_nid_postfix(int d, std::uint64_t request, void* argument); +void* APS5_VABI opendir_nid_postfix(const char* path); +void* APS5_VABI fdopen_nid_postfix(int d, const char* mode); +int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioWaitRequest(std::int32_t id, std::int32_t* state, std::uint32_t* usec); +int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); +int APS5_VABI flock_nid_postfix(int d, int operation); +int APS5_VABI _fstatfs_nid_postfix(int d, void* buf); +int APS5_VABI ftruncate_nid_postfix(int d, std::int64_t length); +int APS5_VABI sceKernelFsync(int d); +int APS5_VABI sceKernelFdatasync(int d); +int APS5_VABI sceKernelGetdents(int d, char* buf, int nbytes); +int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode); +int APS5_VABI sceKernelFutimes(int d, const void* times); +void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept; +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); +std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix(); +using TerminateHandler = void (*)(); +TerminateHandler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(TerminateHandler); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "app0.zar kernel check failed at line %d\n", line); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +void OnMmapRefused() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + const bool refused = type != nullptr && error != nullptr && std::strcmp(type->name(), typeid(std::runtime_error).name()) == 0 && + std::strstr(error->what(), "mmap on a file read from app0.zar") != nullptr && std::strstr(error->what(), "not implemented") != nullptr; + std::_Exit(refused ? 0 : 1); +} + +template bool NotImplemented(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), "not implemented") != nullptr; + } + return false; +} + +constexpr std::size_t BigSize = 3 * 65536 + 77; +constexpr std::uint16_t TypeMask = 0170000; +constexpr std::uint16_t RegularType = 0100000; +constexpr std::uint16_t DirectoryType = 0040000; +constexpr int DuplicateFrom = 0; +constexpr std::uint64_t ReadableRequest = 0x4004667f; + +void Archive(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", { + {"Data/Big.bin", AppArchiveFixture::Pattern(BigSize)}, + {"Data/Small.txt", {'z', 'a', 'r'}}, + {"sce_sys/Packed.json", {'{', '}'}}, + {"eboot.bin", {'o', 'l', 'd'}}, + }); + std::ofstream eboot(root / "app0" / "eboot.bin", std::ios::binary); + eboot << "disk"; +} + +void Reads() { + const auto expected = AppArchiveFixture::Pattern(BigSize); + const int fd = sceKernelOpen("/app0/data/big.BIN", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + std::vector buffer(BigSize + 16); + Check(sceKernelRead(fd, buffer.data(), 70000) == 70000); + Check(std::equal(buffer.begin(), buffer.begin() + 70000, expected.begin())); + Check(sceKernelLseek(fd, 0, 1) == 70000); + Check(lseek_nid_postfix(fd, 0, 1) == 70000); + + std::vector left(10), right(65536); + const KernelIovec vector[] = {{left.data(), left.size()}, {right.data(), right.size()}}; + Check(sceKernelReadv(fd, vector, 2) == 65546); + Check(std::equal(left.begin(), left.end(), expected.begin() + 70000) && std::equal(right.begin(), right.end(), expected.begin() + 70010)); + Check(sceKernelPreadv(fd, vector, 2, 5) == 65546); + Check(std::equal(left.begin(), left.end(), expected.begin() + 5) && std::equal(right.begin(), right.end(), expected.begin() + 15)); + Check(sceKernelLseek(fd, 0, 1) == 135546); + + Check(sceKernelPread(fd, buffer.data(), 100, 65500) == 100 && std::equal(buffer.begin(), buffer.begin() + 100, expected.begin() + 65500)); + Check(pread_nid_postfix(fd, buffer.data(), 100, BigSize - 40) == 40 && std::equal(buffer.begin(), buffer.begin() + 40, expected.end() - 40)); + Check(sceKernelPread(fd, buffer.data(), 1, -1) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelLseek(fd, 0, 1) == 135546); + + int available = 0; + Check(ioctl_nid_postfix(fd, ReadableRequest, &available) == 0 && available == static_cast(BigSize - 135546)); + + Check(sceKernelLseek(fd, -7, 2) == static_cast(BigSize - 7)); + Check(sceKernelRead(fd, buffer.data(), 100) == 7 && std::equal(buffer.begin(), buffer.begin() + 7, expected.end() - 7)); + Check(sceKernelRead(fd, buffer.data(), 100) == 0); + Check(sceKernelLseek(fd, -1, 0) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelLseek(fd, 0, 1) == static_cast(BigSize)); + Check(sceKernelWrite(fd, "x", 1) == SCE_KERNEL_ERROR_EBADF); + + FileStat status{}; + Check(sceKernelFstat(fd, &status) == 0); + Check((status.st_mode & TypeMask) == RegularType && status.st_size == static_cast(BigSize) && status.st_nlink == 1); + + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{65530, 20, buffer.data(), &result, fd}; + std::int32_t id = 0; + std::int32_t state = 0; + Check(sceKernelAioSubmitReadCommands(&request, 1, 0, &id) == 0); + Check(sceKernelAioWaitRequest(id, &state, nullptr) == 0); + Check(result.return_value == 20 && std::equal(buffer.begin(), buffer.begin() + 20, expected.begin() + 65530)); + std::int32_t removed = 0; + Check(sceKernelAioDeleteRequest(id, &removed) == 0); + + Check(sceKernelLseek(fd, 1000, 0) == 1000); + const int duplicate = dup_nid_postfix(fd); + Check(duplicate >= 0 && sceKernelLseek(duplicate, 0, 1) == 1000); + Check(sceKernelRead(duplicate, buffer.data(), 24) == 24 && sceKernelLseek(fd, 0, 1) == 1024); + const int above = fcntl_nid_postfix(fd, DuplicateFrom, 200); + Check(above >= 200 && sceKernelLseek(above, 0, 1) == 1024); + Check(sceKernelClose(fd) == 0); + Check(sceKernelRead(duplicate, buffer.data(), 4) == 4 && std::equal(buffer.begin(), buffer.begin() + 4, expected.begin() + 1024)); + Check(sceKernelLseek(above, 0, 1) == 1028); + Check(close_nid_postfix(above) == 0); + + const int plain = sceKernelOpen("/app0/eboot.bin", SCE_KERNEL_O_RDONLY, 0); + Check(plain >= 0); + Check(dup2_nid_postfix(plain, duplicate) == duplicate); + Check(sceKernelPread(duplicate, buffer.data(), 8, 0) == 4 && std::memcmp(buffer.data(), "disk", 4) == 0); + Check(sceKernelClose(plain) == 0); + Check(sceKernelClose(duplicate) == 0); +} + +void Status() { + FileStat status{}; + Check(sceKernelStat("/app0/Data/Small.txt", &status) == 0); + Check((status.st_mode & TypeMask) == RegularType && (status.st_mode & 0222) == 0 && status.st_size == 3 && status.st_blocks == 1); + Check(sceKernelStat("/app0/data", &status) == 0 && (status.st_mode & TypeMask) == DirectoryType); + Check(sceKernelStat("/app0/Data/", &status) == 0); + Check(sceKernelStat("/app0/Data/Small.txt/", &status) == SCE_KERNEL_ERROR_ENOTDIR); + Check(sceKernelStat("/app0/Data/Missing.txt", &status) == SCE_KERNEL_ERROR_ENOENT); + Check(sceKernelStat("/app0/eboot.bin", &status) == 0 && status.st_size == 4); + Check(sceKernelCheckReachability("/app0/Data/Small.txt") == 0); + Check(sceKernelCheckReachability("/app0/Data/Missing.txt") == SCE_KERNEL_ERROR_ENOENT); +} + +void Refusals() { + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDWR, 0) == SCE_KERNEL_ERROR_EROFS); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_TRUNC, 0) == SCE_KERNEL_ERROR_EROFS); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0666) == SCE_KERNEL_ERROR_EEXIST); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0) == SCE_KERNEL_ERROR_ENOTDIR); + Check(sceKernelOpen("/app0/Data/Missing.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); + Check(NotImplemented([] { (void)sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); + Check(NotImplemented([] { (void)sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); + Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/sce_sys"); })); + Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/Data"); })); + const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + Check(NotImplemented([fd] { (void)fdopen_nid_postfix(fd, "r"); })); + Check(NotImplemented([fd] { (void)flock_nid_postfix(fd, 1); })); + Check(NotImplemented([fd] { alignas(8) char statfs[512]; (void)_fstatfs_nid_postfix(fd, statfs); })); + Check(NotImplemented([fd] { (void)ftruncate_nid_postfix(fd, 0); })); + Check(NotImplemented([fd] { (void)sceKernelFsync(fd); })); + Check(NotImplemented([fd] { (void)sceKernelFdatasync(fd); })); + Check(NotImplemented([fd] { char entries[512]; (void)sceKernelGetdents(fd, entries, sizeof(entries)); })); + Check(NotImplemented([fd] { (void)sceKernelFchmod(fd, 0444); })); + Check(NotImplemented([fd] { (void)sceKernelFutimes(fd, nullptr); })); + FileStat status{}; + Check(sceKernelFstat(fd, &status) == 0 && status.st_size == 3); + Check(sceKernelClose(fd) == 0); +} + +void WithoutArchive() { + { std::ofstream file(std::filesystem::current_path() / "app0" / "Plain.txt", std::ios::binary); file << "plain"; } + const int fd = sceKernelOpen("/app0/Plain.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + char buffer[8] = {}; + Check(sceKernelRead(fd, buffer, sizeof(buffer)) == 5 && std::memcmp(buffer, "plain", 5) == 0); + Check(sceKernelClose(fd) == 0); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); + FileStat status{}; + Check(sceKernelStat("/app0/Data/Small.txt", &status) == SCE_KERNEL_ERROR_ENOENT); + const int directory = sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(directory >= 0 && sceKernelClose(directory) == 0); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: guest_kernel_app_archive_tests archive|plain\n"); + return 2; + } + const std::string mode = argv[1]; + const auto root = AppArchiveFixture::EnterFreshRoot("anyps5-kernel-app-archive"); + if (mode == "archive") { + Archive(root); + Reads(); + Status(); + Refusals(); + } else if (mode == "mmap") { + Archive(root); + const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + _ZSt13set_terminatePFvvE_nid_postfix(OnMmapRefused); + (void)mmap_nid_postfix(nullptr, 16384, 1, 2, fd, 0); + return 1; + } else if (mode == "plain") { + WithoutArchive(); + } else { + return 2; + } + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 6a76e356e3..44bb942f03 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -29,16 +29,32 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); +int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern, std::uint32_t wait_mode, std::uint64_t* result_pat); +int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern, std::uint32_t wait_mode, std::uint64_t* result_pat, KernelUseconds* timeout); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000B); static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); +static constexpr std::uint32_t EVF_ATTR_SINGLE = 0x10; +static constexpr std::uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr std::uint32_t EVF_WAITMODE_AND = 0x01; +static constexpr std::uint32_t EVF_WAITMODE_OR = 0x02; +static constexpr std::uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; +static constexpr std::uint32_t EVF_WAITMODE_CLEAR_PAT = 0x20; static constexpr int MUTEX_TYPE_ERRORCHECK = 1; static constexpr int PRIO_NONE = 0; static constexpr int PRIO_INHERIT = 1; @@ -156,4 +172,68 @@ int main() { Require(pthread_mutex_lock_nid_postfix(&staticAdaptive) == POSIX_EDEADLK); Require(pthread_mutex_unlock_nid_postfix(&staticAdaptive) == 0); Require(pthread_mutex_destroy_nid_postfix(&staticAdaptive) == 0); + + KernelEventFlag ef = nullptr; + Require(sceKernelCreateEventFlag(nullptr, "test", EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, nullptr, EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, "test_zero_attr", 0, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test_fifo", 0x01, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test_single_fifo", EVF_ATTR_SINGLE | 0x01u, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_SINGLE | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_MULTI | 0x1000u, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelDeleteEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSetEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelClearEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelPollEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + + Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); + std::uint64_t pattern = 0; + Require(sceKernelPollEventFlag(ef, 0, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, 0, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_KERNEL_ERROR_EINVAL); + + Require(sceKernelPollEventFlag(ef, 0x20, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x15); + Require(sceKernelPollEventFlag(ef, 0x11, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(pattern == 0x15); + + Require(sceKernelClearEventFlag(ef, ~std::uint64_t{0x01}) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x01, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x14); + + Require(sceKernelSetEventFlag(ef, 0x02) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x06, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_OK); + Require(pattern == 0x16); + Require(sceKernelPollEventFlag(ef, 0x02, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x10); + + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_ALL, &pattern) == SCE_OK); + Require(pattern == 0x10); + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0); + + KernelUseconds efTimeout = 1000; + Require(sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_OR, &pattern, &efTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(pattern == 0); + + int waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x42, &waitThreads) == SCE_OK); + Require(waitThreads == 0); + Require(sceKernelPollEventFlag(ef, 0x42, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(pattern == 0x42); + + Require(sceKernelClearEventFlag(ef, 0) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x42, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0); + + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 679a739b64..912ce45b64 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -287,8 +287,13 @@ static void RecordLocksBelongToTheProcess() { 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})); + Require(NoConflict(reader, {2, 3, 0, Unlock, FromStart, 9})); + Require(sceKernelLseek(file, 7, 0) == 7); + GuestLock currentUnlock{-3, 2, 0, Unlock, FromCurrent, 0}; + Require(fcntl_nid_postfix(file, GetLock, ¤tUnlock) == 0); + Require(currentUnlock.type == Unlock && currentUnlock.start == 4 && currentUnlock.length == 2 && currentUnlock.whence == FromCurrent); +#ifdef _WIN32 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); diff --git a/core/libs/tests/GuestKernelIsStack.cpp b/core/libs/tests/GuestKernelIsStack.cpp index 941ffdc975..968cdc981f 100644 --- a/core/libs/tests/GuestKernelIsStack.cpp +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -13,7 +13,11 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf } static constexpr int SCE_OK = 0; +#ifdef ANYPS5_ENABLE_ASAN +static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000010FC00000000ull; +#else static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000000FC00000000ull; +#endif static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000d); static int global = 0; diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index f946e321ae..8df5bb6381 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -154,7 +154,38 @@ static void* APS5_VABI WriteLeftGuard(void*) { return nullptr; } +#ifdef _WIN32 +static void ProtectWholeStack() { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto stackBytes = reinterpret_cast(end) - reinterpret_cast(start); + Require(mprotect_nid_postfix(start, stackBytes, 0xc2) == 0); + Require(QueryPage(reinterpret_cast(start)).writable); + static_cast(start)[0] = 6; +} + +static DWORD WINAPI ProtectReservedStack(void*) { + ProtectWholeStack(); + return 0; +} + +static void ProtectHostStacks() { + ProtectWholeStack(); + HANDLE thread = CreateThread(nullptr, HostBytes * 4, ProtectReservedStack, nullptr, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + DWORD exitCode = 1; + Require(GetExitCodeThread(thread, &exitCode) != 0 && exitCode == 0); + Require(CloseHandle(thread) != 0); +} +#endif + int main() { +#ifdef _WIN32 + ProtectHostStacks(); +#endif std::atomic passed{false}; Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Worker, &passed, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 8ac99378e5..5c3f4416d5 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #ifdef _WIN32 #include #include @@ -23,6 +24,7 @@ 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); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); 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*); @@ -57,11 +59,16 @@ 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 EvDispatch = 0x0080; +static constexpr std::uint16_t EvForceoneshot = 0x0100; static constexpr std::uint16_t EvError = 0x4000; +static constexpr std::uint16_t EvEof = 0x8000; static constexpr std::uint32_t NoteTrigger = 0x01000000; +static constexpr std::uint32_t NoteLowat = 0x0001; 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 NoteNseconds = 0x0008; static constexpr std::uint32_t NoteDelete = 0x0001; static constexpr std::uint32_t NoteWrite = 0x0002; static constexpr std::uint32_t NoteExtend = 0x0004; @@ -162,6 +169,12 @@ static void Timers() { Require(Poll(kq, events, 4) == 0); RequireFailure(Apply(kq, Change(2, EvfiltTimer, EvDelete)), GuestEnoent); + Require(Apply(kq, Change(5, EvfiltTimer, EvAdd | EvOneshot, NoteNseconds, 1500000001)) == 0); + const KernelTimespec twoSeconds{2, 0}; + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &twoSeconds) == 1); + Require(events[0].ident == 5 && events[0].data == 1); + Require(Poll(kq, events, 4) == 0); + 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); @@ -206,6 +219,40 @@ static void UserEvents() { Require(close_nid_postfix(kq) == 0); } +static void PipeRead() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto readEnd = static_cast(pipe[0]); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(readEnd, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == readEnd && events[0].filter == EvfiltRead && events[0].data == 3 && events[0].udata == &marker && (events[0].flags & EvEof) == 0); + char buffer[4]{}; + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(Poll(kq, events, 4) == 0); + std::thread writer([&pipe] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(write_nid_postfix(pipe[1], "d", 1) == 1); + }); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + writer.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == readEnd && events[0].data == 1); + Require(close_nid_postfix(pipe[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 1 && (events[0].flags & EvEof) != 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void SocketPair() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -225,12 +272,122 @@ static void SocketPair() { 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)); })); + const int space = kqueue_nid_postfix(); + Require(Apply(space, Change(static_cast(pair[1]), EvfiltWrite, EvAdd)) == 0); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite); + const std::intptr_t idle = events[0].data; + Require(idle > 0); + const std::string payload(1000, 'p'); + Require(send_nid_postfix(pair[1], payload.data(), payload.size(), 0) == static_cast(payload.size())); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite); +#if defined(__linux__) + Require(events[0].data <= idle - static_cast(payload.size())); +#else + Require(events[0].data > 0 && events[0].data <= idle); +#endif + std::string drained(payload.size(), '\0'); + Require(recv_nid_postfix(pair[0], drained.data(), drained.size(), 0) == static_cast(drained.size())); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite && events[0].data == idle); + Require(Apply(space, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)) == 0); + Require(close_nid_postfix(space) == 0); 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 LowWaterMarks() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto reader = static_cast(pair[0]); + KernelEvent events[4]{}; + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 4)) == 0); + Require(send_nid_postfix(pair[1], "ab", 2, 0) == 2); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "cd", 2, 0) == 2); + Require(Wait(kq, events, 4) == 1 && events[0].ident == reader && events[0].filter == EvfiltRead && events[0].data == 4); + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 8)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 4); + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 8)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot, NoteLowat, 1)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); + Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd, NoteLowat, 2)); })); + Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); + int datagrams[2]; + Require(socketpair_nid_postfix(Unix, 2, 0, datagrams) == 0); + const auto datagram = static_cast(datagrams[0]); + Require(Throws([kq, datagram] { Apply(kq, Change(datagram, EvfiltRead, EvAdd, NoteLowat, 2)); })); + Require(Apply(kq, Change(datagram, EvfiltRead, EvAdd, NoteLowat, 1)) == 0); + Require(send_nid_postfix(datagrams[1], "f", 1, 0) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].ident == datagram && events[0].data == 1); + Require(close_nid_postfix(datagrams[0]) == 0 && close_nid_postfix(datagrams[1]) == 0); + Require(close_nid_postfix(pair[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == reader && (events[0].flags & EvEof) != 0); + Require(close_nid_postfix(pair[0]) == 0); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd, NoteLowat, 64)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "e", 1) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 1); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void DisableAndDispatch() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + int marker = 0; + KernelEvent events[4]{}; + const auto ident = static_cast(pair[0]); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvDisable)) == 0); + Require(send_nid_postfix(pair[1], "a", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].data == 1); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvDisable)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + char buffer[4]{}; + Require(recv_nid_postfix(pair[0], buffer, 1, 0) == 1); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvDispatch, 0, 0, &marker)) == 0); + Require(send_nid_postfix(pair[1], "bc", 2, 0) == 2); + Require(Wait(kq, events, 4) == 1 && events[0].udata == &marker && events[0].data == 2); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "d", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvEnable)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvForceoneshot)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvForceoneshot)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltRead, EvDelete)), GuestEnoent); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + + Require(Apply(kq, Change(9, EvfiltUser, EvAdd | EvDisable)) == 0); + Require(Apply(kq, Change(9, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(9, EvfiltUser, EvAdd | EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].filter == EvfiltUser); + Require(Apply(kq, Change(9, EvfiltUser, EvDelete)) == 0); + + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void Errors() { const int kq = kqueue_nid_postfix(); KernelEvent events[4]{}; @@ -249,10 +406,14 @@ static void Errors() { 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)); })); + Require(Apply(kq, Change(1, EvfiltUser, EvAdd | EvDisable)) == 0); + Require(Apply(kq, Change(1, EvfiltUser, EvDelete)) == 0); + RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, -1, nullptr), GuestEinval); int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); - Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltRead, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltWrite, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd | EvClear)); })); 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}; @@ -447,9 +608,14 @@ static std::string Utf8(const std::wstring& text) { } static PACL DenyListing(const std::wstring& directory, PSECURITY_DESCRIPTOR& owner) { - wchar_t account[256]{}; - DWORD count = static_cast(sizeof(account) / sizeof(account[0])); - Require(::GetUserNameW(account, &count) != FALSE); + HANDLE token = nullptr; + Require(::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token) != FALSE); + DWORD size = 0; + ::GetTokenInformation(token, TokenUser, nullptr, 0, &size); + std::vector user(size); + const BOOL queried = ::GetTokenInformation(token, TokenUser, user.data(), size, &size); + ::CloseHandle(token); + Require(queried != FALSE); PACL original = nullptr; Require(::GetNamedSecurityInfoW(directory.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &original, nullptr, &owner) == ERROR_SUCCESS); @@ -457,9 +623,9 @@ static PACL DenyListing(const std::wstring& directory, PSECURITY_DESCRIPTOR& own denied.grfAccessPermissions = FILE_LIST_DIRECTORY; denied.grfAccessMode = DENY_ACCESS; denied.grfInheritance = NO_INHERITANCE; - denied.Trustee.TrusteeForm = TRUSTEE_IS_NAME; + denied.Trustee.TrusteeForm = TRUSTEE_IS_SID; denied.Trustee.TrusteeType = TRUSTEE_IS_USER; - denied.Trustee.ptstrName = account; + denied.Trustee.ptstrName = static_cast(reinterpret_cast(user.data())->User.Sid); PACL blocked = nullptr; Require(::SetEntriesInAclW(1, &denied, original, &blocked) == ERROR_SUCCESS); const DWORD applied = ::SetNamedSecurityInfoW(const_cast(directory.c_str()), SE_FILE_OBJECT, @@ -486,7 +652,24 @@ static void RequireContextInsensitive(std::string message, std::string context) } } +static void DropPrivilege(HANDLE token, const wchar_t* name) { + TOKEN_PRIVILEGES privileges{}; + privileges.PrivilegeCount = 1; + privileges.Privileges[0].Attributes = SE_PRIVILEGE_REMOVED; + Require(::LookupPrivilegeValueW(nullptr, name, &privileges.Privileges[0].Luid) != FALSE); + Require(::AdjustTokenPrivileges(token, FALSE, &privileges, 0, nullptr, nullptr) != FALSE); +} + +static void DropBackupPrivileges() { + HANDLE token = nullptr; + Require(::OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token) != FALSE); + DropPrivilege(token, L"SeBackupPrivilege"); + DropPrivilege(token, L"SeRestorePrivilege"); + Require(::CloseHandle(token) != FALSE); +} + static void DeniedParentWatch() { + DropBackupPrivileges(); const auto path = TemporaryName("denied"); Require(mkdir_nid_postfix(path.c_str(), 0755) == 0); const int file = open_nid_postfix((path + "/watched.bin").c_str(), ReadWrite | Create, 0644); @@ -530,9 +713,12 @@ static void Timeout() { int main() { CreateAndClose(); Timers(); + PipeRead(); UserEvents(); SocketPair(); + DisableAndDispatch(); Errors(); + LowWaterMarks(); FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp index 07bb8719c3..467d2e6ed5 100644 --- a/core/libs/tests/GuestLibcCndTimedWait.cpp +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -99,6 +99,26 @@ int main() { } signaler.join(); + constexpr std::int64_t farSeconds = 10000000000; + const Xtime farTime{farSeconds, 999999999}; + constexpr auto clockSeconds = std::chrono::duration_cast(std::chrono::system_clock::duration::max()).count(); + if (farSeconds < clockSeconds) { + bool farReady = false; + std::thread farSignaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + farReady = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + while (!farReady) { + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &farTime) == success); + } + farSignaler.join(); + } else { + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &farTime); }); + } + Require(_Mtx_unlock_nid_postfix(&mutex) == success); RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, nullptr); }); const Xtime overflowCarryTime{std::numeric_limits::max(), 1000000000}; diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 93cf43ff11..04ddac5f38 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -13,6 +13,8 @@ extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_postfix(); GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix(); +struct GuestLocaleFacade { GuestLocale::Implementation* implementation; }; +GuestLocaleFacade* APS5_VABI _ZNSt6locale7classicEv_nid_postfix(); void APS5_VABI _ZNSt6locale5facet9_RegisterEv_nid_postfix(GuestLocale::Facet* self); void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName); void APS5_VABI _ZNSt8_LocinfoD1Ev_nid_postfix(GuestLocale::LocinfoStorage* self); @@ -49,6 +51,9 @@ static void CheckGuestCalls() { Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix()); Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); + auto* classic = _ZNSt6locale7classicEv_nid_postfix(); + Require(classic != nullptr && classic->implementation == locale); + Require(_ZNSt6locale7classicEv_nid_postfix() == classic); const std::byte* table = nullptr; std::memcpy(&table, locale, sizeof(table)); void (APS5_VABI *retain)(GuestLocale::Facet*) = nullptr; diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 7eaeb5619f..cd631caa4e 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -16,6 +16,8 @@ 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); @@ -31,10 +33,15 @@ int main() { Require(sceLoginDialogTerminate() == kErrNotInitialized); Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogClose() == kErrNotInitialized); Require(sceLoginDialogGetResult(result) == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); + Require(sceLoginDialogUpdateStatus() == kStatusNone); Require(sceLoginDialogInitialize() == 0); Require(sceLoginDialogInitialize() == kErrAlreadyInitialized); + Require(sceLoginDialogGetStatus() == kStatusInitialized); + Require(sceLoginDialogUpdateStatus() == kStatusInitialized); Require(sceLoginDialogOpen(nullptr) == kErrArgNull); Require(sceLoginDialogGetResult(result) == kErrNotFinished); @@ -51,4 +58,5 @@ int main() { Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestLoginService.cpp b/core/libs/tests/GuestLoginService.cpp new file mode 100644 index 0000000000..a1fa20d143 --- /dev/null +++ b/core/libs/tests/GuestLoginService.cpp @@ -0,0 +1,31 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceLoginServiceInitialize(void); +int APS5_VABI sceLoginServiceTerminate(void); +int APS5_VABI sceLoginServiceRequestDevices(void* param); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +int main() { + Require(sceLoginServiceInitialize() == 0); + Require(sceLoginServiceTerminate() == 0); + unsigned char buffer[0x80]{}; + try { + sceLoginServiceRequestDevices(buffer); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == "sceLoginServiceRequestDevices not implemented") return 0; + std::fprintf(stderr, "sceLoginServiceRequestDevices threw: %s\n", error.what()); + return 1; + } + std::fprintf(stderr, "sceLoginServiceRequestDevices returned instead of reporting that it is not implemented\n"); + return 1; +} diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index e068f31ef2..3ab7ee3685 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -81,6 +81,7 @@ 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); +extern int signgam_nid_postfix; float APS5_VABI logbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); @@ -108,6 +109,7 @@ 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 __isfinitel_nid_postfix(long double); int APS5_VABI __isnormal_nid_postfix(double); int APS5_VABI __isnormalf_nid_postfix(float); int APS5_VABI __isinff_nid_postfix(float); @@ -923,6 +925,23 @@ int main() { 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); + for (const double sign : {1.0, -1.0}) { + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::abs(lgamma_nid_postfix(sign * std::numeric_limits::denorm_min()) - 744.44007192138122) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-1025) - 710.47586007394398) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-1022) - 708.39641853226408) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-57) - 39.509389291916882) < 1e-13); + Require(std::fetestexcept(FE_OVERFLOW | FE_DIVBYZERO | FE_INVALID) == 0); + } + const auto gammaSign = [](double x) { + signgam_nid_postfix = 7; + lgamma_nid_postfix(x); + return signgam_nid_postfix; + }; + Require(gammaSign(0.5) == 1 && gammaSign(10.) == 1 && gammaSign(0.) == 1 && gammaSign(-0.) == -1); + Require(gammaSign(-0.5) == -1 && gammaSign(-1.5) == 1 && gammaSign(-2.5) == -1 && gammaSign(-0x1p-80) == -1); + Require(gammaSign(-1.) == 1 && gammaSign(-2.) == 1 && gammaSign(-0x1p60) == 1 && gammaSign(-4503599627370495.5) == 1); + Require(gammaSign(INFINITY) == 1 && gammaSign(-INFINITY) == 1 && gammaSign(NAN) == 1); Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); for (const int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { @@ -1002,6 +1021,21 @@ int main() { Require(__isinf_nid_postfix(0.) == 0 && __isinf_nid_postfix(1e308 * 10) == 1); Require(__isfinitef_nid_postfix(0.f) == 1 && __isfinitef_nid_postfix(infinity) == 0); Require(__isfinitef_nid_postfix(nan) == 0); + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + for (const long double value : {0.0L, -0.0L, 1.0L, -1.0L, std::numeric_limits::denorm_min(), + -std::numeric_limits::denorm_min(), std::numeric_limits::min(), + std::numeric_limits::max(), std::numeric_limits::lowest(), + 0x1p1024L, -0x1p1024L, 0x1p-16382L}) { + Require(__isfinitel_nid_postfix(value) == 1); + } + for (const long double value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), -std::numeric_limits::quiet_NaN(), + std::numeric_limits::signaling_NaN(), -std::numeric_limits::signaling_NaN()}) { + Require(__isfinitel_nid_postfix(value) == 0); + } + Require(std::fetestexcept(FE_ALL_EXCEPT) == FE_DIVBYZERO); + std::feclearexcept(FE_ALL_EXCEPT); Require(__isnormalf_nid_postfix(1.f) == 1 && __isnormalf_nid_postfix(0.f) == 0); Require(__isnormalf_nid_postfix(std::numeric_limits::denorm_min()) == 0); Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a7460faf47..49c51e1e93 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -203,6 +203,16 @@ static void CheckAudioCoprocessorProtection() { Require(rejected && undefined == nullptr); } +static void CheckMprotectIgnoresUndefinedBits() { + constexpr std::size_t length = 0x4000; + void* mapping = nullptr; + Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); + Require(sceKernelMprotect(mapping, length, static_cast(0xfffffff2u)) == 0); + static_cast(mapping)[length - 1] = 5; + Require(static_cast(mapping)[length - 1] == 5); + Require(sceKernelMunmap(mapping, length) == 0); +} + static void CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -912,6 +922,7 @@ static DWORD ImageProtection(const void* pointer) { Require(VirtualQuery(pointer, &memory, sizeof(memory)) == sizeof(memory)); return memory.Protect & 0xffu; } +#endif static unsigned char* GuestModulePage(void* module, std::size_t page) { auto* data = static_cast(dlsym_nid_postfix(module, "guestMemoryModuleData")); @@ -926,6 +937,7 @@ static bool RegisteredGuestRange(const void* pointer, std::size_t bytes) { return mutation.Overlaps(pointer, bytes); } +#ifdef _WIN32 static void CheckGuestModuleImageProtection() { constexpr std::size_t page = 0x4000; const auto path = std::filesystem::relative(GUEST_MEMORY_ELF0_MODULE).generic_string(); @@ -1032,6 +1044,54 @@ static void CheckGuestModuleImageProtection() { Require(!RegisteredGuestRange(plainTarget, page)); Require(dlclose_nid_postfix(plainModule) == 0); } +#elif defined(__linux__) +static std::string MappingPermissions(const void* pointer) { + const auto address = reinterpret_cast(pointer); + std::ifstream maps("/proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + unsigned long begin = 0; + unsigned long end = 0; + char permissions[5]{}; + if (std::sscanf(line.c_str(), "%lx-%lx %4s", &begin, &end, permissions) == 3 && address >= begin && address < end) return permissions; + } + return {}; +} + +static void CheckGuestModuleImageProtection() { + constexpr std::size_t page = 0x4000; + const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + void* module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + auto* target = GuestModulePage(module, page); + Require(!RegisteredGuestRange(target, page)); + Require(sceKernelMprotect(target, page, 1) == 0); + Require(RegisteredGuestRange(target, page)); + Require(MappingPermissions(target).starts_with("r--")); + Require(sceKernelMprotect(target, page, 3) == 0); + Require(MappingPermissions(target).starts_with("rw-")); + target[1] = 7; + Require(target[1] == 7); + Require(dlclose_nid_postfix(module) == 0); + Require(!RegisteredGuestRange(target, page)); + + module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + target = GuestModulePage(module, page); + Require(!RegisteredGuestRange(target, page)); + Require(sceKernelMprotect(target, page, 1) == 0); + Require(RegisteredGuestRange(target, page)); + Require(dlclose_nid_postfix(module) == 0); + Require(!RegisteredGuestRange(target, page)); + + bool rejected = false; + try { + sceKernelMprotect(reinterpret_cast(&::getpid), page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); +} #endif static void CheckUnhintedKernelMappingsLandInTheWindow() { @@ -1207,6 +1267,28 @@ static void CheckAliasAcrossDirectAllocations() { Require(sceKernelReleaseDirectMemory(first, page * 3) == 0); } +static void CheckAliasLeavesResidentRunsUnderAHostWrite() { + 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; + { + const GuestArena::HostWrite open(static_cast(mapped) + page, page); + Require(open.Open()); + Require(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 2) == nullptr); + bytes[page + 5] = 0x22; + } + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 2)); + Require(alias != nullptr && alias[5] == 0x11 && alias[page + 5] == 0x22); + GuestArena::GuestArenaUnmapAlias_nid_postfix(const_cast(alias)); + Require(bytes[5] == 0x11 && bytes[page + 5] == 0x22); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + static void CheckAliasRefusesPagesThatAreNotSharedViews() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -2362,6 +2444,36 @@ static void CheckVirtualQuerySplitFlexibleRanges() { Require(sceKernelMunmap(bytes + page * 2, page) == 0); } +static void CheckVirtualQueryWireLayout() { + constexpr std::size_t page = 0x4000; + constexpr char name[] = "query wire layout"; + void* mapped = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&mapped, page, 3, 0, name) == 0); + struct { + std::array before; + VirtualQueryInfo info; + std::array after; + } result{}; + result.before.fill(0xa5); + result.after.fill(0x5a); + Require(sceKernelVirtualQuery(mapped, 0, &result.info, 72) == 0); + const auto* bytes = reinterpret_cast(&result.info); + std::uintptr_t start = 0, end = 0; + std::int32_t protection = 0; + std::memcpy(&start, bytes, sizeof(start)); + std::memcpy(&end, bytes + 8, sizeof(end)); + std::memcpy(&protection, bytes + 24, sizeof(protection)); + Require(start == reinterpret_cast(mapped) && end == start + page); + Require(protection == 3 && bytes[32] == 0x11); // flexible and committed + Require(std::memcmp(bytes + 33, name, sizeof(name)) == 0); + Require(bytes[65] == 0 && bytes[66] == 0); + Require(std::all_of(bytes + 67, bytes + 72, [](auto byte) { return byte == 0; })); + Require(std::all_of(result.before.begin(), result.before.end(), [](auto byte) { return byte == 0xa5; })); + Require(std::all_of(result.after.begin(), result.after.end(), [](auto byte) { return byte == 0x5a; })); + Require(sceKernelClearVirtualRangeName(mapped, page) == 0); + Require(sceKernelMunmap(mapped, page) == 0); +} + static void CheckVirtualQueryForNonReadableGuestRange() { constexpr std::size_t page = 0x4000; auto* mapped = static_cast(mmap_nid_postfix(nullptr, page, 3, 0x1002, -1, 0)); @@ -2557,12 +2669,14 @@ int main(int argc, char** argv) { CheckVirtualQueryPartialMunmap(); CheckMunmapOverUnmappedPages(); CheckPageTableStats(); + CheckVirtualQueryWireLayout(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); + CheckMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); CheckReleaseDirectMemoryClearsMappings(); @@ -2588,11 +2702,14 @@ int main(int argc, char** argv) { CheckHeapAfterMappingReuse(); CheckNoOverwriteRejectsHostOccupiedMapping(); CheckFixedMappingsReachTheApplicationAreaEnd(); -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) CheckGuestModuleImageProtection(); +#endif +#ifdef _WIN32 CheckAliasAcrossDirectAllocations(); CheckAliasOfReorderedSharedPages(); CheckAliasRefusesPagesThatAreNotSharedViews(); + CheckAliasLeavesResidentRunsUnderAHostWrite(); #endif CheckUnhintedKernelMappingsLandInTheWindow(); #if defined(__linux__) diff --git a/core/libs/tests/GuestMessageDestructorAbi.cpp b/core/libs/tests/GuestMessageDestructorAbi.cpp index edf1e8cd41..68e398ef4d 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.cpp +++ b/core/libs/tests/GuestMessageDestructorAbi.cpp @@ -20,10 +20,16 @@ extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; extern const unsigned char _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix[]; extern const unsigned char _ZTISt12out_of_range_nid_postfix[]; extern const unsigned char _ZTISt11regex_error_nid_postfix[]; +extern const unsigned char _ZTISt13runtime_error_nid_postfix[]; +extern const unsigned char _ZTISt11logic_error_nid_postfix[]; void APS5_VABI _ZNSt12out_of_rangeC1EPKc_nid_postfix(Object*, const char*); void APS5_VABI _ZNSt12out_of_rangeC1ERKS__nid_postfix(Object*, const Object*); void APS5_VABI _ZNSt12out_of_rangeD1Ev_nid_postfix(Object*); +void APS5_VABI _ZNSt11logic_errorC1EPKc_nid_postfix(Object*, const char*); +void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(Object*); [[noreturn]] void APS5_VABI _ZSt14_Xout_of_rangePKc_nid_postfix(const char*); +[[noreturn]] void APS5_VABI _ZSt15_Xruntime_errorPKc_nid_postfix(const char*); +[[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const Object*); [[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type); int APS5_VABI GuestOuter(); void APS5_VABI GuestStdCapture(); @@ -206,25 +212,40 @@ int main(int argc, char** argv) { return 0; } const bool regex = std::strstr(argv[1], "regex") != nullptr; + const bool runtime = std::strstr(argv[1], "runtime") != nullptr; + const bool doraise = std::strstr(argv[1], "doraise") != nullptr; regularRelease = std::strcmp(argv[1], "release") == 0; keepMessage = std::strstr(argv[1], "unshared") == nullptr; FixtureArgument = reinterpret_cast(FixtureMessage); + Object raised{}; if (regex) { expectedMessage = "regular expression error"; FixtureStdOutType = _ZTISt11regex_error_nid_postfix; FixtureThrow = reinterpret_cast(_ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix); FixtureArgument = static_cast(std::regex_constants::error_collate); + } else if (runtime) { + expectedMessage = FixtureMessage; + FixtureStdOutType = _ZTISt13runtime_error_nid_postfix; + FixtureThrow = reinterpret_cast(_ZSt15_Xruntime_errorPKc_nid_postfix); + } else if (doraise) { + _ZNSt11logic_errorC1EPKc_nid_postfix(&raised, FixtureMessage); + expectedMessage = FixtureMessage; + FixtureStdOutType = _ZTISt11logic_error_nid_postfix; + FixtureThrow = reinterpret_cast(_ZNKSt11logic_error8_DoraiseEv_nid_postfix); + FixtureArgument = reinterpret_cast(&raised); } _ZNSt12out_of_rangeC1EPKc_nid_postfix(&decoy, "unrelated message"); poison = std::strstr(argv[1], "zero") ? nullptr : &decoy; GuestStdCapture(); + const char* expected = regex ? "regular expression error" : FixtureMessage; if (callbacks != (regularRelease ? 0u : 1u) || (keepMessage && (MessageHeader(retained)->references.load() != 0 || - std::strcmp(retained.message, regex ? "regular expression error" : FixtureMessage))) || + std::strcmp(retained.message, expected))) || !decoy.message || std::strcmp(decoy.message, "unrelated message")) return 44; _ZNSt12out_of_rangeD1Ev_nid_postfix(&retained); _ZNSt12out_of_rangeD1Ev_nid_postfix(&decoy); - if (retained.message || decoy.message) return 45; + if (doraise) _ZNSt11logic_errorD1Ev_nid_postfix(&raised); + if (retained.message || decoy.message || (doraise && raised.message)) return 45; std::puts("guest message callback, retained ownership and release: PASS"); return 0; } diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 8bad49dd13..79b571e9b2 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -22,6 +22,7 @@ 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 munlockall_nid_postfix(); int* APS5_VABI __error_nid_postfix(); } @@ -110,6 +111,18 @@ int main() { #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); #endif + Require(mlock_nid_postfix(mapped, page) == 0); +#ifdef HAS_LOCKED_KILOBYTES + Require(LockedKilobytes() - lockedBefore == page / 1024); +#endif + *__error_nid_postfix() = untouched; + Require(munlockall_nid_postfix() == 0); + Require(*__error_nid_postfix() == untouched); +#ifdef HAS_LOCKED_KILOBYTES + Require(LockedKilobytes() == lockedBefore); +#elif defined(_WIN32) + Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); +#endif *__error_nid_postfix() = 0; Require(mlock_nid_postfix(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2) == -1); @@ -144,5 +157,9 @@ int main() { RequireLockAllUnsupported(1); RequireLockAllUnsupported(2); RequireLockAllUnsupported(1 | 2); + + *__error_nid_postfix() = untouched; + Require(munlockall_nid_postfix() == 0); + Require(*__error_nid_postfix() == untouched); return 0; } diff --git a/core/libs/tests/GuestMprotectUnmapped.cpp b/core/libs/tests/GuestMprotectUnmapped.cpp index ed26e5bfa3..8a4ed05199 100644 --- a/core/libs/tests/GuestMprotectUnmapped.cpp +++ b/core/libs/tests/GuestMprotectUnmapped.cpp @@ -56,6 +56,12 @@ int main() { unsigned char* memory = Map(3); Require(munmap_nid_postfix(memory + Page, Page) == 0, "munmap of the middle page failed"); + Require(Protect(memory, 0, ProtectRead) == 0, "a zero-length protection of a mapped page must succeed"); + Require(ProtectionAt(memory) == ProtectReadWrite, "a zero-length protection must not change the page"); + Require(Protect(memory + Page, 0, ProtectRead) == 0 && Protect(nullptr, 0, ProtectRead) == 0, "a zero-length protection of unmapped memory must succeed"); + Require(sceKernelMtypeprotect(memory, 0, 0, ProtectRead) == 0 && ProtectionAt(memory) == ProtectReadWrite, "a zero-length sceKernelMtypeprotect must succeed without changing the page"); + Require(mprotect_nid_postfix(memory + Page, 0, ProtectRead) == 0, "a zero-length mprotect of unmapped memory must succeed"); + Require(Protect(memory, Page * 3, ProtectRead) == 0, "a range with a gap in the middle must succeed for its mapped parts"); Require(ProtectionAt(memory) == ProtectRead && ProtectionAt(memory + Page * 2) == ProtectRead, "the mapped parts on both sides of the gap must be protected"); Require(mprotect_nid_postfix(memory, Page * 3, ProtectReadWrite) == 0, "mprotect over a range with a gap must succeed for its mapped parts"); diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index dd43d10b4c..b25419c969 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -22,20 +22,32 @@ void Require(bool value) { if (!value) std::abort(); } constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004u); +constexpr int kErrNotInitialized = static_cast(0x80B80003u); +constexpr int kErrNotFinished = static_cast(0x80B80005u); constexpr int kErrNotRunning = static_cast(0x80B8000Bu); +constexpr int kErrArgNull = static_cast(0x80B8000Du); constexpr int kButtonIdOk = 1; } int main() { std::uint8_t param[0x60]{}; + MsgDialogResult result{}; + Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogUpdateStatus() == 0); Require(sceMsgDialogGetStatus() == 0); Require(sceMsgDialogInitialize() == 0); + Require(sceMsgDialogInitialize() == kErrAlreadyInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogGetResult(&result) == kErrNotFinished); + Require(sceMsgDialogGetResult(nullptr) == kErrArgNull); + Require(sceMsgDialogOpen(nullptr) == kErrArgNull); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); Require(sceMsgDialogGetStatus() == kStatusInitialized); @@ -50,13 +62,16 @@ int main() { Require(sceMsgDialogUpdateStatus() == kStatusFinished); Require(sceMsgDialogGetStatus() == kStatusFinished); - MsgDialogResult result{}; + result = MsgDialogResult{}; result.button_id = -1; Require(sceMsgDialogGetResult(&result) == 0); Require(result.result == 0); Require(result.button_id == kButtonIdOk); Require(sceMsgDialogTerminate() == 0); + Require(sceMsgDialogTerminate() == kErrNotInitialized); + Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetStatus() == 0); } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index f0196de5a7..5730ecd1a9 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -20,6 +20,7 @@ extern "C" { int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); } @@ -113,6 +114,7 @@ int main() { Require(QueryWord(4) == 1); Require(QueryWord(11) == 0); Require(QueryWord(19) == 0); + Require(QueryWord(21) == 0); const auto address = QueryAddress(14); const auto host = ntohl(address.s_addr); @@ -124,7 +126,12 @@ int main() { QueryAddress(17); QueryAddress(18); - for (const int code : {5, 6, 7, 8, 9, 10, 12, 13, 20, 21}) { + for (const int code : {12, 13, 20}) { + std::memset(&info, 0x5a, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0 && info.opaque[0] == 0); + } + + for (const int code : {5, 6, 7, 8, 9, 10}) { bool threw = false; try { sceNetCtlGetInfo(code, &info); @@ -133,4 +140,8 @@ int main() { } Require(threw); } + + Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); + NetCtlNatInfo nat_info{}; + Require(sceNetCtlGetNatInfo(&nat_info) == kErrNotConnected); } diff --git a/core/libs/tests/GuestNewHandler.cpp b/core/libs/tests/GuestNewHandler.cpp index cb1a0364e1..04a5d9a8a1 100644 --- a/core/libs/tests/GuestNewHandler.cpp +++ b/core/libs/tests/GuestNewHandler.cpp @@ -15,9 +15,11 @@ Handler APS5_VABI _ZSt15set_new_handlerPFvvE_nid_postfix(Handler); Handler APS5_VABI _ZSt15get_new_handlerv_nid_postfix(); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _Znam_nid_postfix(std::size_t); +void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t, std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void APS5_VABI _ZdlPv_nid_postfix(void*); +void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void*, std::size_t); } namespace { @@ -27,6 +29,7 @@ unsigned failuresLeft = 0; unsigned allocations = 0; unsigned handlerCalls = 0; std::size_t lastSize = 0; +std::size_t lastAlignment = 0; void Require(bool condition, int line) { if (!condition) { @@ -50,7 +53,10 @@ void* APS5_VABI allocate(std::size_t bytes) { void APS5_VABI release(void*) {} void* APS5_VABI allocateZeroed(std::size_t count, std::size_t bytes) { return allocate(count * bytes); } void* APS5_VABI reallocate(void*, std::size_t bytes) { return allocate(bytes); } -void* APS5_VABI align(std::size_t, std::size_t bytes) { return allocate(bytes); } +void* APS5_VABI align(std::size_t alignment, std::size_t bytes) { + lastAlignment = alignment; + return allocate(bytes); +} void* APS5_VABI realign(void*, std::size_t bytes, std::size_t) { return allocate(bytes); } int APS5_VABI posixAlign(void** pointer, std::size_t, std::size_t bytes) { *pointer = allocate(bytes); @@ -176,6 +182,20 @@ int main(int argc, char** argv) { REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(nullptr) == &allocatingHandler); REQUIRE(_ZSt15get_new_handlerv_nid_postfix() == nullptr); + Reset(0); + void* aligned = _ZnwmSt11align_val_t_nid_postfix(24, 16); + REQUIRE(aligned == storage.data() && lastSize == 24 && lastAlignment == 16 && allocations == 1); + _ZdlPvSt11align_val_t_nid_postfix(aligned, 16); + Reset(1); + REQUIRE(Throws([] { _ZnwmSt11align_val_t_nid_postfix(8, 32); }) && handlerCalls == 0 && allocations == 1); + + Reset(0); + REQUIRE(_ZnwmSt11align_val_t_nid_postfix(0, 16) == storage.data() && lastSize == 1 && lastAlignment == 16); + Reset(2); + _ZSt15set_new_handlerPFvvE_nid_postfix(&countingHandler); + REQUIRE(_ZnwmSt11align_val_t_nid_postfix(8, 16) == storage.data() && handlerCalls == 2 && allocations == 3); + _ZSt15set_new_handlerPFvvE_nid_postfix(nullptr); + std::puts("Guest new handler checks passed"); return 0; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index eaa8a77d45..a89ed3ea2e 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -4,9 +4,14 @@ #include #include #include +#include +#include extern "C" { int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id); +int APS5_VABI sceNpGetUserIdByAccountId(std::uint64_t account_id, int* user_id); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void); } extern "C" { @@ -67,5 +72,25 @@ int main() { Require(sceNpGetNpId(0x10000, &npId) == SignedOut, "sceNpGetNpId must report the user as signed out"); Require(std::memcmp(&npId, &untouched, sizeof(npId)) == 0, "sceNpGetNpId must leave the NpId untouched"); Require(sceNpGetNpId(0x10000, nullptr) == InvalidArgument, "sceNpGetNpId must reject a null NpId"); + int userId = 0x5a5a; + Require(sceNpGetUserIdByAccountId(0, &userId) == InvalidArgument, "sceNpGetUserIdByAccountId must reject account id 0"); + Require(sceNpGetUserIdByAccountId(0x1234, nullptr) == InvalidArgument, "sceNpGetUserIdByAccountId must reject a null output"); + Require(sceNpGetUserIdByAccountId(0x1234, &userId) == SignedOut, "sceNpGetUserIdByAccountId must report NP as signed out"); + Require(userId == 0x5a5a, "sceNpGetUserIdByAccountId must leave the user id untouched"); + unsigned char buffer[0x11]{}; + try { + sceNpUnknown_8weiQyIdI3A(0x10000, buffer); + Require(false, "sceNpUnknown_8weiQyIdI3A must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpUnknown_8weiQyIdI3A not implemented", + "sceNpUnknown_8weiQyIdI3A threw"); + } + try { + sceNpUnknown_WxLcN76q19o(); + Require(false, "sceNpUnknown_WxLcN76q19o must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpUnknown_WxLcN76q19o not implemented", + "sceNpUnknown_WxLcN76q19o threw"); + } return 0; } diff --git a/core/libs/tests/GuestNpPartner.cpp b/core/libs/tests/GuestNpPartner.cpp index 0121f30c88..700570470a 100644 --- a/core/libs/tests/GuestNpPartner.cpp +++ b/core/libs/tests/GuestNpPartner.cpp @@ -5,6 +5,7 @@ extern "C" { int APS5_VABI sceNpEAAccessInitialize(void); int APS5_VABI sceNpEAAccessTerminate(void); +int APS5_VABI sceNpHasEAAccessSubscription(int, bool*); } namespace { @@ -24,11 +25,20 @@ bool Throws(TAction action) { } int main() { + bool subscribed = true; Require(Throws([] { sceNpEAAccessTerminate(); })); + Require(Throws([&] { sceNpHasEAAccessSubscription(1, &subscribed); })); Require(sceNpEAAccessInitialize() == 0); Require(Throws([] { sceNpEAAccessInitialize(); })); + Require(Throws([] { sceNpHasEAAccessSubscription(1, nullptr); })); + Require(sceNpHasEAAccessSubscription(1, &subscribed) == 0); + Require(!subscribed); + subscribed = true; + Require(sceNpHasEAAccessSubscription(0, &subscribed) == 0); + Require(!subscribed); Require(sceNpEAAccessTerminate() == 0); Require(Throws([] { sceNpEAAccessTerminate(); })); + Require(Throws([&] { sceNpHasEAAccessSubscription(1, &subscribed); })); Require(sceNpEAAccessInitialize() == 0); Require(sceNpEAAccessTerminate() == 0); } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index b01b7e1902..f6504104c1 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -16,6 +16,7 @@ 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 sceNpWebApi2PushEventAbortHandle(int, int); int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int, int, std::uint32_t); int APS5_VABI sceNpWebApi2CreateUserContext(int, int); int APS5_VABI sceNpWebApi2CreateRequest(int, const char*, const char*, const char*, const void*, std::int64_t*); @@ -46,6 +47,8 @@ int main() { Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, nullptr); })); const int handle = sceNpWebApi2PushEventCreateHandle(context); Require(handle > 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle + 1000) == 0); Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); const int user = sceNpWebApi2CreateUserContext(context, 1); std::int64_t request = 0; @@ -54,6 +57,7 @@ int main() { Require(sceNpWebApi2DeleteRequest(request) == 0); Require(sceNpWebApi2Terminate(context) == 0); Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); + Require(Throws([&] { sceNpWebApi2PushEventAbortHandle(context, handle); })); Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); Require(sceNpWebApi2Terminate(other) == 0); return 0; diff --git a/core/libs/tests/GuestOperationMode.cpp b/core/libs/tests/GuestOperationMode.cpp index 248ec491ac..b3fe29f518 100644 --- a/core/libs/tests/GuestOperationMode.cpp +++ b/core/libs/tests/GuestOperationMode.cpp @@ -3,6 +3,7 @@ #include extern "C" { void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode); +int APS5_VABI sceKernelIsTrinityMode(void); } static void Require(bool value) { if (!value) std::abort(); } static bool Throws(int* mode, int* submode) { @@ -15,6 +16,7 @@ int main() { Require(out[0] == 2 && out[1] == -1); Require(out[2] == 0 && out[3] == -1); int value = -1; - Require(Throws(nullptr, &value) && Throws(&value, nullptr)); + Require(Throws(nullptr, &value) && Throws(&value, nullptr) && Throws(nullptr, nullptr)); Require(value == -1); + Require(sceKernelIsTrinityMode() == 0); } diff --git a/core/libs/tests/GuestOpusCeltDec.cpp b/core/libs/tests/GuestOpusCeltDec.cpp index 29c139a019..aa3572b7e1 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -43,6 +43,35 @@ const std::uint8_t CELT_STEREO[] = { const std::uint8_t HYBRID_PACKET[] = {0x7C, 0x87, 0xFC, 0xB1}; +const std::uint8_t CELT_TONE[4][60] = { + {0xFC, 0x9F, 0xF9, 0x33, 0x84, 0xFB, 0x41, 0x76, 0xA8, 0x21, 0xCE, 0x57, 0xBB, 0x16, 0x49, 0x05, 0x21, 0x3F, 0x2A, 0xC4, + 0x79, 0xD0, 0x73, 0x62, 0x89, 0x7E, 0xF9, 0x29, 0x75, 0x1E, 0xAC, 0x80, 0xAE, 0x30, 0xF1, 0x46, 0x18, 0x1F, 0xF8, 0x8B, + 0xD0, 0xC4, 0x37, 0xF1, 0x12, 0x47, 0x01, 0xF9, 0x43, 0xA4, 0x1A, 0x8C, 0x80, 0x40, 0xF0, 0x0A, 0xC3, 0xFD, 0x4C, 0xB1}, + {0xFC, 0x9E, 0xA4, 0xC8, 0xDF, 0x47, 0x28, 0xAE, 0x34, 0xFD, 0x32, 0x11, 0xB9, 0x69, 0x99, 0x7B, 0x61, 0xC6, 0x21, 0x94, + 0x0E, 0x20, 0xED, 0xC2, 0x95, 0x07, 0x78, 0x0D, 0x00, 0x92, 0x42, 0x12, 0xD4, 0xE9, 0xEC, 0x5E, 0xDC, 0xF4, 0xB9, 0x7A, + 0x2E, 0x23, 0xA1, 0xC6, 0x4C, 0xD7, 0x69, 0xEC, 0x34, 0x8F, 0x78, 0x21, 0x35, 0xF1, 0x1B, 0xB0, 0xF0, 0xF5, 0x5C, 0xD1}, + {0xFC, 0x9E, 0xA6, 0x12, 0xDC, 0x36, 0xC3, 0x4B, 0x2C, 0x3D, 0x85, 0x17, 0x32, 0xE1, 0x56, 0x62, 0xFE, 0x39, 0xDB, 0x85, + 0xFD, 0x83, 0x0D, 0x90, 0xF9, 0x32, 0xEB, 0xEB, 0x38, 0xFD, 0x3D, 0x96, 0x26, 0x21, 0xE6, 0x38, 0x6A, 0x8F, 0xDD, 0x1F, + 0x68, 0xBD, 0xAF, 0x4F, 0x9D, 0x08, 0x16, 0xED, 0x7B, 0x2A, 0x62, 0x4C, 0x7A, 0x85, 0xAB, 0x7B, 0x0F, 0x3D, 0x43, 0xD1}, + {0xFC, 0x7F, 0xDF, 0x09, 0x18, 0xD2, 0x71, 0xA1, 0xE6, 0x6C, 0x70, 0xED, 0xCA, 0x2F, 0x47, 0x79, 0xA7, 0xEA, 0xE3, 0xD1, + 0x9B, 0xCD, 0x9E, 0x87, 0xE5, 0xE0, 0x0A, 0x86, 0x0A, 0xEE, 0x8E, 0x63, 0xE4, 0x77, 0x45, 0xE9, 0xF0, 0x82, 0xF8, 0x2A, + 0x13, 0xD2, 0xDB, 0x17, 0x11, 0x2E, 0x36, 0x10, 0x03, 0x30, 0x20, 0x00, 0xF7, 0x42, 0xD8, 0x71, 0x7E, 0xB0, 0xF3, 0x3C}, +}; + +void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { + for (int channel = 0; channel < channels; ++channel) { + int peak = 0, crossings = 0; + for (int sample = 0; sample < samples; ++sample) { + const int value = pcm[sample * channels + channel]; + peak = std::max(peak, std::abs(value)); + if (sample > 0 && (value < 0) != (pcm[(sample - 1) * channels + channel] < 0)) ++crossings; + } + Require(peak > 100 && peak < 200); + const int expected = samples * 2000 / rate; + Require(crossings >= expected - 4 && crossings <= expected + 4); + } +} + } // namespace int main() { @@ -77,6 +106,25 @@ int main() { offset += bytes + 2; } Require(audible); + + std::array unsupported{}; + for (int rate : {0, 22050, 44100, 96000}) + RequireThrows([&] { sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2); }); + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + std::array toneState{}; + Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0); + const int samples = 960 * rate / 48000; + std::array tone{}; + for (int frame = 0; frame < 4; ++frame) { + tone.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4 - 1); }); + Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4) == samples * 4); + Require(tone[0] == 12345 && tone[samples * 2 + 1] == 12345); + if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, 2, rate); + } + Require(sceOpusCeltDecDestroy(toneState.data()) == 0); + } + Require(sceOpusCeltDecDestroy(state.data()) == 0); RequireThrows([&] { sceOpusCeltDecDestroy(state.data()); }); RequireThrows([&] { sceOpusCeltDecDecode(state.data(), CELT_STEREO + 2, CELT_STEREO[0], pcm.data(), 3840); }); diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index 0b462ff31c..a5824ee855 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -62,7 +62,7 @@ int main() { RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); }); RequireThrows([&] { sceOpusDecGetSize(3); }); std::array unsupported{}; - for (int rate : {8000, 12000, 16000, 24000}) + for (int rate : {0, 22050, 44100, 96000}) RequireThrows([&] { sceOpusDecCreateEx(&context, unsupported.data(), rate, 2); }); std::array pcm{}, reference{}; pcm.fill(12345); @@ -99,6 +99,25 @@ int main() { Require(std::any_of(mono.begin() + 1, mono.end() - 1, [](auto value) { return value != 0; })); offset += bytes + 2; } + for (int rate : {8000, 12000, 16000, 24000}) { + std::array rateState{}; + Require(sceOpusDecCreateEx(&context, rateState.data(), rate, 2) == 0); + const int samples = 960 * rate / 48000; + std::array resampled{}; + int peak = 0; + for (std::size_t at = 0, frame = 0; frame < 3; ++frame) { + const int bytes = OPUS_STEREO[at] | (OPUS_STEREO[at + 1] << 8); + resampled.fill(12345); + RequireThrows([&] { sceOpusDecDecode(rateState.data(), OPUS_STEREO + at + 2, bytes, resampled.data() + 1, samples * 4 - 1); }); + Require(sceOpusDecDecode(rateState.data(), OPUS_STEREO + at + 2, bytes, resampled.data() + 1, samples * 4) == samples * 4); + Require(resampled[0] == 12345 && resampled[samples * 2 + 1] == 12345); + for (int index = 1; index <= samples * 2; ++index) peak = std::max(peak, std::abs(static_cast(resampled[index]))); + at += bytes + 2; + } + Require(peak > 900 && peak < 1200); + Require(sceOpusDecDestroy(rateState.data()) == 0); + } + Require(sceOpusDecDestroy(state.data()) == 0); RequireThrows([&] { sceOpusDecDestroy(state.data()); }); RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840); }); diff --git a/core/libs/tests/GuestPPSA01530Exports.cpp b/core/libs/tests/GuestPPSA01530Exports.cpp deleted file mode 100644 index ee9470613e..0000000000 --- a/core/libs/tests/GuestPPSA01530Exports.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" - -#include -#include -#include - -extern "C" { -int APS5_VABI sceNetShowIfconfig(); -int APS5_VABI scePadUnknown_n3kSX62fgNo(); -} - -namespace { - -struct Export { - const char* name; - int (APS5_VABI *function)(); -}; - -const Export exports[] = { - {"sceNetShowIfconfig", sceNetShowIfconfig}, - {"scePadUnknown_n3kSX62fgNo", scePadUnknown_n3kSX62fgNo}, -}; - -bool ReportsNotImplemented(const Export& entry) { - try { - entry.function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(entry.name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", entry.name, error.what()); - return false; - } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", entry.name); - return false; -} - -} - -int main() { - bool correct = true; - for (const auto& entry : exports) correct &= ReportsNotImplemented(entry); - return correct ? 0 : 1; -} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index f30bd4d91d..84b0ad50ab 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -7,7 +7,10 @@ #include #include #include +#include +#include #include +#include extern "C" { int APS5_VABI scePadOpen_nid_postfix(int, int, int, const void*); @@ -17,6 +20,7 @@ int APS5_VABI scePadGetHandle(int, int, int); int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool); int APS5_VABI scePadSetVibrationMode(int, int); int APS5_VABI scePadInit_nid_postfix(void); +int APS5_VABI scePadUnknown_n3kSX62fgNo(); int APS5_VABI scePadRead_nid_postfix(int, PadData*, int); int APS5_VABI scePadReadState(int, PadData*); int APS5_VABI scePadSetTiltCorrectionState(int, bool); @@ -28,6 +32,16 @@ int APS5_VABI scePadIsRemoteController(int, bool*); static void Require(bool value) { if (!value) std::abort(); } +static void RequireUnknownPadNidNotImplemented() { + try { + scePadUnknown_n3kSX62fgNo(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "scePadUnknown_n3kSX62fgNo not implemented"); + return; + } + std::abort(); +} + static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -36,7 +50,7 @@ static void CheckInputCommands() { } cleanup{path}; const auto write = [&](const char* command) { std::ofstream(path) << command << '\n'; }; write("1 16384 200"); - Pad::InputCommandFile commands(path.string().c_str()); + Pad::InputCommandFile commands(path); Require(commands.Buttons(1000) == 0); write("2 16384 200"); Require(commands.Buttons(101000) == 0x4000); @@ -83,9 +97,17 @@ static void CheckInputCommands() { write("7 8192 300 1 2 3 4"); const auto accepted = commands.Poll(2601000); Require(accepted.buttons == 0x2000 && accepted.hasSticks && accepted.sticks[3] == 4); + write("8 2097152 300"); + const auto left = commands.Poll(3001000); + Require(left.buttons == 0 && left.touchLeft && !left.touchRight && !left.hasSticks); + write("9 4210688 300"); + const auto right = commands.Poll(3401000); + Require(right.buttons == 0x4000 && !right.touchLeft && right.touchRight); + write("10 8388608 300"); + Require(commands.Poll(3801000) == none); std::filesystem::remove(path); - Require(commands.Buttons(3001000) == 0); - Pad::InputCommandFile disabled(nullptr); + Require(commands.Buttons(4201000) == 0); + Pad::InputCommandFile disabled(std::filesystem::path{}); Require(disabled.Buttons(0) == 0); } @@ -99,7 +121,7 @@ static float SettleOrientationW() { } static void CheckTiltCorrection(int handle) { - Require(scePadInit_nid_postfix() == 0); + RequireUnknownPadNidNotImplemented(); PadInputState tilted; tilted.hasMotion = true; tilted.accel = {9.80665f, 0.0f, 0.0f}; @@ -235,20 +257,39 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto centred = Pad::ReadState(); Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); Require(centred.timestamp > moved.timestamp); + for (const auto& [command, x] : {std::pair{"3 2097152 300\n", 480}, std::pair{"4 4194304 300\n", 1440}}) { + std::ofstream(path) << command; + PadData touched{}; + const auto touchDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + touched = Pad::ReadState(); + if (touched.touch_data_touch_num != 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < touchDeadline); + Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 471); + Require(touched.buttons == (0x2000 | 0x100000)); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto lifted = Pad::ReadState(); + Require(lifted.touch_data_touch_num == 0 && lifted.buttons == 0x2000); + } PadPublishInput_nid_postfix(PadInputState{}); } int main() { CheckInputCommands(); + Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x80920005)); const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { std::filesystem::path path; ~Cleanup() { std::error_code error; std::filesystem::remove(path, error); } - } cleanup{path}; + }; + auto commandPath = path; + commandPath += std::filesystem::path(u8"-žluťoučký-漢字"); + Cleanup cleanup{commandPath}; #ifdef _WIN32 - Require(_putenv_s("APS5_PAD_INPUT_FILE", path.string().c_str()) == 0); + Require(_wputenv_s(L"APS5_PAD_INPUT_FILE", commandPath.c_str()) == 0); #else - Require(setenv("APS5_PAD_INPUT_FILE", path.string().c_str(), 1) == 0); + Require(setenv("APS5_PAD_INPUT_FILE", commandPath.string().c_str(), 1) == 0); #endif { std::uint32_t sequence = 0; @@ -279,6 +320,8 @@ int main() { Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadInit_nid_postfix() == 0); + Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x809200ff)); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); Require(scePadGetHandle(user, 0, 0) == handle); @@ -292,7 +335,7 @@ int main() { CheckReadQueue(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); - CheckInjectedState(path); + CheckInjectedState(commandPath); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); Require(scePadSetVibrationMode(handle, 3) == 0); diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 77b345b58c..0d24636d4b 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -35,7 +35,8 @@ static void RequireMissingChunkMetadata(Operation&& operation) { const std::string_view message(error.what()); threw = message.find("playgo-chunk.dat") != std::string_view::npos && message.find("playgo-scenario.json") != std::string_view::npos && - message.find("chunk ID") != std::string_view::npos; + message.find("chunk ID") != std::string_view::npos && + message.find("APS5_PLAYGO_UNLISTED_CHUNKS") != std::string_view::npos; } Require(threw); } @@ -149,6 +150,52 @@ static int CheckRejectedLayout(const char* root, const char* fileName) { return 0; } +static void SetUnlistedChunks(const char* value) { +#ifdef _WIN32 + Require(_putenv_s("APS5_PLAYGO_UNLISTED_CHUNKS", value) == 0); +#else + Require(setenv("APS5_PLAYGO_UNLISTED_CHUNKS", value, 1) == 0); +#endif +} + +static int CheckNoPlayGoFileAnswered(const char* root, const char* answer) { + constexpr int badChunkId = static_cast(0x80B2000C); + SetUnlistedChunks(answer); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t unlisted[3] = {0, 1, 999}; + std::int8_t loci[3] = {0, 0, 0}; + const int result = scePlayGoGetLocus(handle, unlisted, 3, loci); + if (std::string_view(answer) == "installed") Require(result == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + else Require(result == badChunkId); + const std::uint16_t first = 0; + Require(scePlayGoGetLocus(handle, &first, 1, loci) == 0 && loci[0] == 3); + Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckUnknownUnlistedChunkAnswer(const char* root) { + SetUnlistedChunks("maybe"); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t unlisted = 1; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &unlisted, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent") != 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); @@ -176,6 +223,9 @@ 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 == "--no-file-installed") return CheckNoPlayGoFileAnswered(argv[2], "installed"); + if (mode == "--no-file-absent") return CheckNoPlayGoFileAnswered(argv[2], "absent"); + if (mode == "--no-file-unknown-answer") return CheckUnknownUnlistedChunkAnswer(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"); @@ -183,6 +233,9 @@ int main(int argc, char** argv) { if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); } RunLayout(argv[0], "--no-file", {}); + RunLayout(argv[0], "--no-file-installed", {}); + RunLayout(argv[0], "--no-file-absent", {}); + RunLayout(argv[0], "--no-file-unknown-answer", {}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index 40a6a6710a..f66e459c7c 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -24,6 +24,7 @@ int main() { Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); + Require(scePlayGoDialogClose() == notInitialized); Require(scePlayGoDialogOpen(nullptr) == paramInvalid); Require(scePlayGoDialogGetResult(nullptr) == notInitialized); diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 7093323415..3287ddd4d9 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -18,6 +18,7 @@ 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_ALREADY_INITIALIZED = static_cast(0x80B80004u); 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); @@ -29,16 +30,19 @@ void Require(bool value) { if (!value) std::abort(); } int main() { std::uint8_t param[64] = {}; std::int32_t result[8] = {}; + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 1738f11dd8..f96ae8f616 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -64,6 +64,10 @@ static void TestPipes(int socket) { 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)); + const auto shortWaitStart = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(&reader, 1, 2) == 0 && reader.revents == 0); + const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(40) && shortWait < std::chrono::milliseconds(200)); 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}; diff --git a/core/libs/tests/GuestPositionedClose.cpp b/core/libs/tests/GuestPositionedClose.cpp new file mode 100644 index 0000000000..d5da32613d --- /dev/null +++ b/core/libs/tests/GuestPositionedClose.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Positioned close check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); + +int main() { + const auto root = std::filesystem::path("anyps5-positioned-close-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto path = (root / "data.bin").string(); + constexpr std::size_t fileSize = 48u << 20; + { + std::ofstream stream(path, std::ios::binary); + const std::vector block(1u << 20, 'x'); + for (std::size_t written = 0; written < fileSize; written += block.size()) stream.write(block.data(), static_cast(block.size())); + Require(stream.good()); + } + std::vector buffer(32u << 20); + int overlapped = 0; + for (int round = 0; round < 60; ++round) { + const int descriptor = sceKernelOpen(path.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(descriptor >= 0); + std::atomic state{0}; + std::atomic threw{false}; + std::int64_t result = 0; + std::thread reader([&] { + state = 1; + try { + result = sceKernelPread(descriptor, buffer.data(), buffer.size(), 0); + } catch (const std::exception&) { + threw = true; + } + state = 2; + }); + while (state.load() == 0) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::microseconds(300)); + const int closed = sceKernelClose(descriptor); + reader.join(); + Require(!threw.load()); + Require(closed == 0); + Require(result == static_cast(buffer.size()) || result == ErrorEbadf); + if (result == static_cast(buffer.size())) ++overlapped; + } + Require(overlapped > 0); + std::filesystem::remove_all(root); + return 0; +} diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 177845674a..938c32f67b 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -30,9 +30,18 @@ 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::size_t); std::int64_t APS5_VABI _write_nid_postfix(int, const void*, std::size_t); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI sceKernelStat(const char*, FileStat*); +int APS5_VABI sceKernelUnlink(const char*); +int APS5_VABI sceKernelTruncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const void*); +int APS5_VABI sceKernelMkdir(const char*, std::uint16_t); +int APS5_VABI sceKernelRmdir(const char*); +int APS5_VABI sceKernelRename(const char*, const char*); } static void Check(bool value, int line) { @@ -121,6 +130,16 @@ int main() { RequirePosixFailure([&] { return _read_nid_postfix(-1, nullptr, 0); }, EBADF); RequirePosixFailure([&] { return write_nid_postfix(-1, nullptr, 0); }, EBADF); RequirePosixFailure([&] { return _write_nid_postfix(-1, nullptr, 0); }, EBADF); + constexpr auto oversized = static_cast(std::numeric_limits::max()) + 1; + RequirePosixFailure([&] { return read_nid_postfix(descriptor, buffer, oversized); }, EINVAL); + RequirePosixFailure([&] { return _read_nid_postfix(descriptor, buffer, oversized); }, EINVAL); + RequirePosixFailure([&] { return write_nid_postfix(descriptor, payload, -1); }, EINVAL); + RequirePosixFailure([&] { return write_nid_postfix(descriptor, payload, std::numeric_limits::min()); }, EINVAL); + RequirePosixFailure([&] { return _write_nid_postfix(descriptor, payload, oversized); }, EINVAL); + RequirePosixFailure([&] { return pread_nid_postfix(descriptor, buffer, oversized, 0); }, EINVAL); + RequirePosixFailure([&] { return pwrite_nid_disambig1_nid_postfix(descriptor, payload, oversized, 0); }, EINVAL); + RequirePosixFailure([&] { return read_nid_postfix(-1, buffer, oversized); }, EINVAL); + RequirePosixFailure([&] { return write_nid_postfix(-1, payload, -1); }, EINVAL); #ifdef _WIN32 errno = EACCES; bool limitDiagnosticPreserved = false; @@ -135,6 +154,23 @@ int main() { Require(sceKernelRead(-1, direct, 1) == static_cast(0x80020009u)); Require(sceKernelWrite(-1, direct, 1) == static_cast(0x80020009u)); Require(sceKernelLseek(-1, 0, 0) == static_cast(0x80020009u)); + Require(sceKernelRead(descriptor, nullptr, 1) == static_cast(0x8002000eu)); + Require(sceKernelWrite(descriptor, nullptr, 1) == static_cast(0x8002000eu)); + Require(sceKernelRead(descriptor, nullptr, 0) == 0); + Require(sceKernelWrite(descriptor, nullptr, 0) == 0); + Require(sceKernelRead(-1, nullptr, 0) == static_cast(0x80020009u)); + Require(sceKernelWrite(-1, nullptr, 0) == static_cast(0x80020009u)); + FileStat nullStat{}; + Require(sceKernelStat(nullptr, &nullStat) == static_cast(0x8002000eu)); + Require(sceKernelStat(nullptr, nullptr) == static_cast(0x8002000eu)); + Require(sceKernelUnlink(nullptr) == static_cast(0x8002000eu)); + Require(sceKernelTruncate_nid_postfix(nullptr, 0) == static_cast(0x8002000eu)); + Require(sceKernelUtimes_nid_postfix(nullptr, nullptr) == static_cast(0x8002000eu)); + Require(sceKernelMkdir(nullptr, 0777) == static_cast(0x8002000eu)); + Require(sceKernelRmdir(nullptr) == static_cast(0x8002000eu)); + Require(sceKernelRename(nullptr, "dest") == static_cast(0x8002000eu)); + Require(sceKernelRename("src", nullptr) == static_cast(0x8002000eu)); + Require(sceKernelRename(nullptr, nullptr) == static_cast(0x8002000eu)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index d0aac1d174..c545232c9b 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -2,11 +2,25 @@ #include #include #include +#include +#include +#include extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param); std::int32_t APS5_VABI scePsmlMfsrCreateContext1100(void** context, const void* param); std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrInit(); +std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const void* param); +std::int32_t APS5_VABI scePsmlMfsrReleaseSharedResources(void* sharedResources); +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context); } namespace { @@ -20,6 +34,164 @@ struct CommandBuffer { std::uint32_t sizeInDwords; }; +using CreateContext = std::int32_t (APS5_VABI*)(void**, const void*); +using GetDispatchPacket = std::int32_t (APS5_VABI*)(void*, void*, const void*); + +void CreateContextUntouched(CreateContext create) { + std::array param{}; + std::uint64_t contextStorage = 0; + void* const sentinel = &contextStorage; + void* context = sentinel; + Require(create(&context, param.data()) == kErrNotInitialized); + Require(context == sentinel); + Require(create(nullptr, nullptr) == kErrNotInitialized); +} + +void DispatchPacketUntouched(GetDispatchPacket dispatch) { + std::array param{}; + std::uint64_t contextStorage = 0; + std::array dwords; + dwords.fill(0xa5a5a5a5u); + const auto untouchedDwords = dwords; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(dispatch(&contextStorage, &commandBuffer, param.data()) == kErrNotInitialized); + Require(dispatch(nullptr, &commandBuffer, nullptr) == kErrNotInitialized); + Require(commandBuffer.cursor == dwords.data()); + Require(commandBuffer.sizeInDwords == dwords.size()); + Require(dwords == untouchedDwords); +} + +template +bool Throws(TCall call, const char* text) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(text) != std::string::npos; + } + return false; +} + +struct Requirement { + std::uint64_t blockBytes; + std::uint64_t alignment; + std::uint64_t blockCount; +}; + +struct Block { + std::uint64_t address; + std::uint64_t bytes; +}; + +struct SharedParam { + std::uint32_t type; + std::uint32_t reserved; + const Block* blocks; + std::uint64_t blockCount; + std::uint64_t reserved2; +}; + +struct ContextParam { + std::array bytes{}; + + void Set(const Block* blocks, std::uint64_t count, void* shared) { + std::memcpy(bytes.data() + 0x08, &blocks, sizeof(blocks)); + std::memcpy(bytes.data() + 0x10, &count, sizeof(count)); + std::memcpy(bytes.data() + 0x38, &shared, sizeof(shared)); + } +}; + +std::array Blocks(const Requirement& requirement, std::uint64_t first) { + std::array blocks{}; + for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = {first + i * requirement.blockBytes, requirement.blockBytes}; + return blocks; +} + +void SetupBeforeInit() { + std::uint64_t storage = 0; + void* const sentinel = &storage; + void* handle = sentinel; + Requirement requirement{1, 2, 3}; + SharedParam sharedParam{}; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == kErrNotInitialized); + Require(requirement.blockBytes == 1 && requirement.alignment == 2 && requirement.blockCount == 3); + Require(scePsmlMfsrCreateSharedResources(&handle, &sharedParam) == kErrNotInitialized); + Require(handle == sentinel); + Require(scePsmlMfsrReleaseContext(sentinel) == kErrNotInitialized); + Require(scePsmlMfsrReleaseSharedResources(sentinel) == kErrNotInitialized); +} + +void SetupAfterInit() { + Require(scePsmlMfsrInit() == 0); + Require(scePsmlMfsrInit() == 0); + + SharedParam sharedParam{}; + Requirement shared{}; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&shared, &sharedParam) == 0); + Require(shared.blockBytes == 0x200000 && shared.alignment == 0x200000 && shared.blockCount == 4); + + const auto sharedBlocks = Blocks(shared, 0x10000000); + sharedParam.blocks = sharedBlocks.data(); + sharedParam.blockCount = sharedBlocks.size(); + void* sharedResources = nullptr; + Require(scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam) == 0); + Require(sharedResources != nullptr); + + ContextParam contextParam; + contextParam.Set(nullptr, 0, sharedResources); + Requirement context{}; + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(&context, contextParam.bytes.data()) == 0); + Require(context.blockBytes == 0x200000 && context.alignment == 0x200000 && context.blockCount == 4); + + const auto contextBlocks = Blocks(context, 0x20000000); + contextParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); + void* mfsrContext = nullptr; + Require(scePsmlMfsrCreateContext800M3_2(&mfsrContext, contextParam.bytes.data()) == 0); + Require(mfsrContext != nullptr && mfsrContext != sharedResources); + + Require(Throws([&] { scePsmlMfsrReleaseSharedResources(sharedResources); }, "still used by a context")); + Require(Throws([&] { scePsmlMfsrReleaseContext(sharedResources); }, "not created by this library")); + Require(scePsmlMfsrReleaseContext(mfsrContext) == 0); + Require(Throws([&] { scePsmlMfsrReleaseContext(mfsrContext); }, "not created by this library")); + Require(scePsmlMfsrReleaseSharedResources(sharedResources) == 0); +} + +void RefusedAfterInit() { + SharedParam sharedParam{}; + Requirement requirement{}; + sharedParam.type = 1; + Require(Throws([&] { scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam); }, "type 1 is not supported")); + + sharedParam.type = 0; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == 0); + const auto blocks = Blocks(requirement, 0x10000000); + sharedParam.blocks = blocks.data(); + sharedParam.blockCount = 3; + void* sharedResources = nullptr; + Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "fewer memory blocks")); + auto small = blocks; + small[2].bytes = 0x100000; + sharedParam.blocks = small.data(); + sharedParam.blockCount = small.size(); + Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "smaller or less aligned")); + Require(sharedResources == nullptr); + + std::uint64_t foreign = 0; + ContextParam contextParam; + contextParam.Set(blocks.data(), blocks.size(), &foreign); + void* context = nullptr; + Require(Throws([&] { scePsmlMfsrCreateContext800M3_2(&context, contextParam.bytes.data()); }, "shared resources handle was not created")); + Require(context == nullptr); + + std::array param{}; + std::array dwords{}; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(Throws([&] { scePsmlMfsrGetContextBufferRequirement1100(&requirement, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrCreateContext1300(&context, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket900(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1000(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1300(&foreign, &commandBuffer, param.data()); }, "not implemented")); +} + } int main() { @@ -50,4 +222,20 @@ int main() { Require(scePsmlMfsrGetDispatchMfsrPacket1100(nullptr, &commandBuffer, param.data()) == kErrNotInitialized); Require(commandBuffer.cursor == dwords.data()); Require(dwords == untouchedDwords); + + std::array olderRequirement; + olderRequirement.fill(0xa5); + const auto untouchedOlderRequirement = olderRequirement; + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(olderRequirement.data(), param.data()) == kErrNotInitialized); + Require(olderRequirement == untouchedOlderRequirement); + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(nullptr, nullptr) == kErrNotInitialized); + + CreateContextUntouched(scePsmlMfsrCreateContext800M3_2); + CreateContextUntouched(scePsmlMfsrCreateContext1300); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket900); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1000); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1300); + SetupBeforeInit(); + SetupAfterInit(); + RefusedAfterInit(); } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 12c5f1b9b1..0b28513f9d 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -27,6 +27,9 @@ int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int in int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope); int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope); int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); +int APS5_VABI pthread_attr_setschedparam_nid_postfix(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int policy); +int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, int* policy); } static constexpr int SCE_OK = 0; @@ -116,6 +119,42 @@ static void CheckSoloSched() { Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == GUEST_EINVAL); } +static void CheckSchedArguments() { + constexpr int SchedFifo = 1; + constexpr int SchedOther = 2; + constexpr int SchedRr = 3; + PthreadAttr attr = nullptr; + PthreadAttr unset = nullptr; + KernelSchedParam param{CREATION_PRIORITY}; + Require(pthread_attr_setschedparam_nid_postfix(nullptr, ¶m) == GUEST_EINVAL); + Require(pthread_attr_setschedparam_nid_postfix(&unset, ¶m) == GUEST_EINVAL); + Require(pthread_attr_setschedpolicy_nid_postfix(&unset, SchedRr) == GUEST_EINVAL); + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setschedparam_nid_postfix(&attr, nullptr) == GUEST_ENOTSUP); + for (const int policy : {SchedFifo, SchedRr}) { + Require(pthread_attr_setschedpolicy_nid_postfix(&attr, policy) == 0); + Require(pthread_attr_setschedparam_nid_postfix(&attr, ¶m) == 0); + for (const int rejected : {0, 1, CREATION_PRIORITY - 1, UPDATED_PRIORITY + 1, 1000}) { + KernelSchedParam outside{rejected}; + Require(pthread_attr_setschedparam_nid_postfix(&attr, &outside) == GUEST_ENOTSUP); + KernelSchedParam reported{-1}; + Require(scePthreadAttrGetschedparam(&attr, &reported) == SCE_OK && reported.sched_priority == CREATION_PRIORITY); + } + KernelSchedParam highest{UPDATED_PRIORITY}; + Require(pthread_attr_setschedparam_nid_postfix(&attr, &highest) == 0); + Require(pthread_attr_setschedparam_nid_postfix(&attr, ¶m) == 0); + } + Require(pthread_attr_setschedpolicy_nid_postfix(&attr, SchedOther) == 0); + KernelSchedParam other{1000}; + Require(pthread_attr_setschedparam_nid_postfix(&attr, &other) == 0); + int policy = -1; + for (const int rejected : {-1, 0, 4, 100}) { + Require(pthread_attr_setschedpolicy_nid_postfix(&attr, rejected) == GUEST_ENOTSUP); + Require(pthread_attr_getschedpolicy_nid_postfix(&attr, &policy) == 0 && policy == SchedOther); + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} + static void* APS5_VABI Worker(void* arg) { static_cast*>(arg)->get(); return nullptr; @@ -220,6 +259,7 @@ static void CheckThreadScope() { int main() { CheckStackSizeLimit(); CheckSoloSched(); + CheckSchedArguments(); CheckSchedulingAttributes(); CheckThreadScope(); PthreadAttr attr = nullptr; diff --git a/core/libs/tests/GuestPthreadAttrStack.cpp b/core/libs/tests/GuestPthreadAttrStack.cpp new file mode 100644 index 0000000000..faf907527a --- /dev/null +++ b/core/libs/tests/GuestPthreadAttrStack.cpp @@ -0,0 +1,73 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackAddress, std::size_t* stackSize); +int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stackAddress); +int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, std::size_t* guardSize); +int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, std::size_t guardSize); +int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* stackAddress, std::size_t stackSize); +int APS5_VABI scePthreadAttrSetstackaddr(PthreadAttr* attr, void* stackAddress); +} + +static constexpr int SCE_OK = 0; +static constexpr int SceEinval = static_cast(0x80020016u); +static constexpr std::size_t DefaultStackSize = 1u << 20; +static constexpr std::size_t MinimumStackSize = 16384; + +static void Require(bool value) { if (!value) std::abort(); } + +alignas(16) static unsigned char stack[65536]; + +static void RequireStack(const PthreadAttr* attr, const void* expectedAddress, std::size_t expectedSize) { + void* address = &stack[1]; + std::size_t size = 1; + Require(scePthreadAttrGetstack(attr, &address, &size) == SCE_OK); + Require(address == expectedAddress && size == expectedSize); +} + +int main() { + PthreadAttr attr = nullptr; + PthreadAttr empty = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + void* address = nullptr; + std::size_t size = 0; + + Require(scePthreadAttrSetstack(nullptr, stack, sizeof(stack)) == SceEinval); + Require(scePthreadAttrSetstack(&empty, stack, sizeof(stack)) == SceEinval); + Require(scePthreadAttrSetstack(&attr, nullptr, sizeof(stack)) == SceEinval); + Require(scePthreadAttrSetstack(&attr, stack, MinimumStackSize - 1) == SceEinval); + RequireStack(&attr, nullptr, DefaultStackSize); + Require(scePthreadAttrSetstack(&attr, stack, MinimumStackSize) == SCE_OK); + RequireStack(&attr, stack, MinimumStackSize); + + Require(scePthreadAttrGetstack(nullptr, &address, &size) == SceEinval); + Require(scePthreadAttrGetstack(&empty, &address, &size) == SceEinval); + Require(scePthreadAttrGetstack(&attr, nullptr, &size) == SceEinval); + Require(scePthreadAttrGetstack(&attr, &address, nullptr) == SceEinval); + + Require(scePthreadAttrSetstackaddr(nullptr, &stack[16]) == SceEinval); + Require(scePthreadAttrSetstackaddr(&empty, &stack[16]) == SceEinval); + Require(scePthreadAttrSetstackaddr(&attr, nullptr) == SceEinval); + Require(scePthreadAttrGetstackaddr(nullptr, &address) == SceEinval); + Require(scePthreadAttrGetstackaddr(&empty, &address) == SceEinval); + Require(scePthreadAttrGetstackaddr(&attr, nullptr) == SceEinval); + Require(scePthreadAttrGetstackaddr(&attr, &address) == SCE_OK && address == stack); + Require(scePthreadAttrSetstackaddr(&attr, &stack[16]) == SCE_OK); + Require(scePthreadAttrGetstackaddr(&attr, &address) == SCE_OK && address == &stack[16]); + + std::size_t guard = 0; + Require(scePthreadAttrSetguardsize(nullptr, 8192) == SceEinval); + Require(scePthreadAttrSetguardsize(&empty, 8192) == SceEinval); + Require(scePthreadAttrSetguardsize(&attr, 8192) == SCE_OK); + Require(scePthreadAttrGetguardsize(nullptr, &guard) == SceEinval); + Require(scePthreadAttrGetguardsize(&empty, &guard) == SceEinval); + Require(scePthreadAttrGetguardsize(&attr, nullptr) == SceEinval); + Require(scePthreadAttrGetguardsize(&attr, &guard) == SCE_OK && guard == 8192); + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} diff --git a/core/libs/tests/GuestPthreadCancel.cpp b/core/libs/tests/GuestPthreadCancel.cpp index 311fb8a5a4..576414c6c3 100644 --- a/core/libs/tests/GuestPthreadCancel.cpp +++ b/core/libs/tests/GuestPthreadCancel.cpp @@ -8,6 +8,7 @@ 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 APS5_VABI scePthreadExit(void* retval); int APS5_VABI scePthreadCancel(Pthread thread); int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); @@ -29,6 +30,8 @@ int APS5_VABI pthread_setcanceltype_nid_postfix(int type, int* old_type); int APS5_VABI sem_init_nid_postfix(void* sem, int pshared, unsigned int value); int APS5_VABI sem_destroy_nid_postfix(void* sem); int APS5_VABI sem_wait_nid_postfix(void* sem); +int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime); +int APS5_VABI sem_getvalue_nid_postfix(void* sem, int* value); void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); void APS5_VABI __pthread_cleanup_pop_imp_nid_postfix(int execute); } @@ -93,6 +96,44 @@ static void* APS5_VABI PosixSemWaiter(void*) { return nullptr; } +struct AvailableTimedSemaphore { + void* semaphore = nullptr; + bool pending; + bool disabled; + bool expired; + bool returned = false; + int result = -2; +}; + +static void* APS5_VABI AvailableTimedSemaphoreWaiter(void* argument) { + auto& value = *static_cast(argument); + if (value.disabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == SCE_OK); + if (value.pending) Require(pthread_cancel_nid_postfix(scePthreadSelf()) == 0); + KernelTimespec deadline{}; + if (!value.expired) + deadline.tv_sec = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count() + 60; + value.result = sem_timedwait_nid_postfix(&value.semaphore, &deadline); + value.returned = true; + return reinterpret_cast(std::intptr_t{7}); +} + +static void CheckAvailableTimedSemaphore(bool pending, bool disabled, bool expired) { + AvailableTimedSemaphore value{nullptr, pending, disabled, expired}; + Require(sem_init_nid_postfix(&value.semaphore, 0, 1) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, AvailableTimedSemaphoreWaiter, &value, nullptr) == SCE_OK); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK); + const bool canceled = pending && !disabled; + Require(result == (canceled ? CANCELED : reinterpret_cast(std::intptr_t{7}))); + Require(value.returned != canceled); + if (!canceled) Require(value.result == 0); + int permits = -1; + Require(sem_getvalue_nid_postfix(&value.semaphore, &permits) == 0); + Require(permits == (canceled ? 1 : 0)); + Require(sem_destroy_nid_postfix(&value.semaphore) == 0); +} + static void* APS5_VABI Idler(void*) { while (!shared.release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); return nullptr; @@ -197,6 +238,11 @@ int main() { Require(pthread_cancel_nid_postfix(thread) == 0); JoinCanceled(thread); + CheckAvailableTimedSemaphore(true, false, false); + CheckAvailableTimedSemaphore(true, false, true); + CheckAvailableTimedSemaphore(true, true, false); + CheckAvailableTimedSemaphore(false, false, true); + Require(scePthreadCreate(&shared.target, nullptr, Idler, nullptr, nullptr) == SCE_OK); thread = Start(Joiner); Require(scePthreadCancel(thread) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp new file mode 100644 index 0000000000..64cfef4b62 --- /dev/null +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -0,0 +1,42 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId); +int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, KernelClockid clockId); +int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared); +} + +static constexpr int SCE_OK = 0; +static constexpr int SceEinval = static_cast(0x80020016u); +static constexpr int GuestEinval = 22; +static constexpr KernelClockid Monotonic = 4; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadCondattr unset = nullptr; + Require(scePthreadCondattrSetclock(nullptr, Monotonic) == SceEinval); + Require(scePthreadCondattrSetclock(&unset, Monotonic) == SceEinval); + Require(scePthreadCondattrDestroy(nullptr) == SceEinval); + Require(scePthreadCondattrDestroy(&unset) == SceEinval); + Require(pthread_condattr_setclock_nid_postfix(&unset, Monotonic) == GuestEinval); + Require(pthread_condattr_setpshared_nid_postfix(nullptr, 0) == GuestEinval); + Require(pthread_condattr_setpshared_nid_postfix(&unset, 0) == GuestEinval); + + PthreadCondattr attr = nullptr; + Require(scePthreadCondattrInit(&attr) == SCE_OK); + for (const KernelClockid clock : {0, 1, 2, Monotonic, 5, 12}) { + Require(scePthreadCondattrSetclock(&attr, clock) == SCE_OK); + Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == 0); + } + for (const int sharing : {0, 1}) + Require(pthread_condattr_setpshared_nid_postfix(&attr, sharing) == 0); + for (const int sharing : {-1, 2}) + Require(pthread_condattr_setpshared_nid_postfix(&attr, sharing) == GuestEinval); + Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); + Require(scePthreadCondattrDestroy(&attr) == SceEinval); +} diff --git a/core/libs/tests/GuestPthreadGetname.cpp b/core/libs/tests/GuestPthreadGetname.cpp index 4943093901..f8545f6078 100644 --- a/core/libs/tests/GuestPthreadGetname.cpp +++ b/core/libs/tests/GuestPthreadGetname.cpp @@ -10,6 +10,7 @@ int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name); int APS5_VABI pthread_getname_np_nid_postfix(Pthread thread, char* name); 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 scePthreadGetname(Pthread thread, char* name); } static constexpr int GUEST_ESRCH = 3; @@ -35,6 +36,10 @@ static void* APS5_VABI Unnamed(void*) { return nullptr; } +static void* APS5_VABI ExitNow(void*) { + return nullptr; +} + int main() { const Pthread self = pthread_self_nid_postfix(); for (const char* name : {"", "A", "AnyPS5Probe", "0123456789012345678901234567890"}) { @@ -45,6 +50,21 @@ int main() { Require(scePthreadCreate(&worker, nullptr, Unnamed, nullptr, nullptr) == 0); Require(scePthreadJoin(worker, nullptr) == 0); + // A joined thread answers pthread_getname_np with ESRCH (PS5 firmware 13.60) and leaves the + // output buffer untouched, so the posix wrapper must not read the retained object's name. + Pthread joined = nullptr; + Require(scePthreadCreate(&joined, nullptr, ExitNow, nullptr, "JoinedWorker") == 0); + Require(scePthreadJoin(joined, nullptr) == 0); + const auto afterJoin = Read(joined, GUEST_ESRCH); + for (unsigned char byte : afterJoin) Require(byte == 0xAA); + + // The SCE sibling has no measured joined-thread behaviour on the console; it still reads the + // retained object's name, which only pins today's behaviour of scePthreadGetname. + std::array sibling{}; + sibling.fill(0x5A); + Require(scePthreadGetname(joined, sibling.data()) == 0); + Require(std::memcmp(sibling.data(), "JoinedWorker", sizeof("JoinedWorker")) == 0); + const auto untouched = Read(nullptr, GUEST_ESRCH); for (unsigned char byte : untouched) Require(byte == 0xAA); Require(pthread_getname_np_nid_postfix(self, nullptr) == GUEST_EFAULT); diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index f3a7257808..75ba65971c 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -7,6 +7,8 @@ extern "C" { int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr); +int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared); +int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared); int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol); int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling); int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling); @@ -77,10 +79,56 @@ static void CheckType() { Require(pthread_mutexattr_gettype_nid_postfix(nullptr, &type) == GUEST_EINVAL && type == -1); } +static void CheckProcessSharing() { + PthreadMutexattr attr = nullptr; + int sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(nullptr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_setpshared_nid_postfix(nullptr, 0) == GUEST_EINVAL); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); + Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + Require(pthread_mutexattr_settype_nid_postfix(&attr, 2) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 1) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); + for (const int invalid : {-1, 2, 2147483647}) + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); + int type = -1; + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 2); + Require(Throws([&] { pthread_mutexattr_getpshared_nid_postfix(&attr, nullptr); })); + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); +} + +static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { + int ceiling = -1; + Require(pthread_mutexattr_setprotocol_nid_postfix(attr, PRIO_PROTECT) == GUEST_EINVAL); + Require(pthread_mutexattr_setprotocol_nid_postfix(attr, -1) == GUEST_EINVAL); + Require(pthread_mutexattr_setprioceiling_nid_postfix(attr, 300) == GUEST_EINVAL); + Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, &ceiling) == GUEST_EINVAL && ceiling == -1); + Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, nullptr) == GUEST_EINVAL); +} + int main() { CheckType(); + CheckProcessSharing(); CheckTimedlockInvalidDeadline(); PthreadMutexattr attr = nullptr; + CheckInvalidPriorityAttribute(nullptr); + CheckInvalidPriorityAttribute(&attr); Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL && ceiling == -1); @@ -102,7 +150,6 @@ int main() { Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL); Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, nullptr); })); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); - Require(Throws([&] { pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300); })); - Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling); })); + CheckInvalidPriorityAttribute(&attr); return 0; } diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 4680fca3bf..1274b6be3a 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include extern "C" { @@ -379,10 +378,8 @@ int main() { Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); - bool rejected = false; - try { pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); Require(rwlock == DestroyedRwlock); diff --git a/core/libs/tests/GuestPthreadSecondJoin.cpp b/core/libs/tests/GuestPthreadSecondJoin.cpp new file mode 100644 index 0000000000..4db4d57034 --- /dev/null +++ b/core/libs/tests/GuestPthreadSecondJoin.cpp @@ -0,0 +1,65 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadDetach(Pthread thread); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003u); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int GUEST_ESRCH = 3; +static constexpr std::size_t REUSED_SLOT_ROUNDS = 64; + +static void Require(bool value) { if (!value) std::abort(); } + +static void* APS5_VABI Finish(void*) { + return reinterpret_cast(std::uintptr_t{0x1234}); +} + +static void* APS5_VABI WaitForRelease(void* arg) { + auto* release = static_cast*>(arg); + while (!release->load()) std::this_thread::yield(); + return nullptr; +} + +int main() { + Pthread thread = nullptr; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, Finish, nullptr, "second-join") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + + result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(thread, &result) == SCE_KERNEL_ERROR_ESRCH); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + Require(scePthreadDetach(thread) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_join_nid_postfix(thread, &result) == GUEST_ESRCH); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + + for (std::size_t round = 0; round < REUSED_SLOT_ROUNDS; ++round) { + thread = nullptr; + result = nullptr; + Require(scePthreadCreate(&thread, nullptr, Finish, nullptr, "reused-slot") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + Require(scePthreadJoin(thread, nullptr) == SCE_KERNEL_ERROR_ESRCH); + } + + static std::atomic release{false}; + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, nullptr, WaitForRelease, &release, "detached") == SCE_OK); + Require(scePthreadDetach(detached) == SCE_OK); + result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(detached, &result) == SCE_KERNEL_ERROR_EINVAL); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + Require(scePthreadDetach(detached) == SCE_KERNEL_ERROR_EINVAL); + release = true; +} diff --git a/core/libs/tests/GuestPthreadTimedLocks.cpp b/core/libs/tests/GuestPthreadTimedLocks.cpp new file mode 100644 index 0000000000..ae2a8187fa --- /dev/null +++ b/core/libs/tests/GuestPthreadTimedLocks.cpp @@ -0,0 +1,85 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); +int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); +int APS5_VABI pthread_mutex_init_nid_postfix(PthreadMutex* mutex, const PthreadMutexattr* attr); +int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_unlock_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime); +} + +static constexpr int SCE_OK = 0; +static constexpr int GUEST_EDEADLK = 11; +static constexpr auto HoldTime = std::chrono::milliseconds(50); + +static void Require(bool value) { if (!value) std::abort(); } + +struct Holder { + void* lock; + bool mutex; + std::atomic held{false}; + std::atomic releasing{false}; + Pthread thread = nullptr; +}; + +static void* APS5_VABI Hold(void* arg) { + auto& holder = *static_cast(arg); + auto* mutex = static_cast(holder.lock); + auto* rwlock = static_cast(holder.lock); + Require((holder.mutex ? pthread_mutex_lock_nid_postfix(mutex) : pthread_rwlock_wrlock_nid_postfix(rwlock)) == 0); + holder.held.store(true); + std::this_thread::sleep_for(HoldTime); + holder.releasing.store(true); + Require((holder.mutex ? pthread_mutex_unlock_nid_postfix(mutex) : pthread_rwlock_unlock_nid_postfix(rwlock)) == 0); + return nullptr; +} + +template +static void RequireWaitsForHolder(void* lock, bool mutex, Lock acquire) { + Holder holder{lock, mutex}; + Require(scePthreadCreate(&holder.thread, nullptr, Hold, &holder, nullptr) == SCE_OK); + while (!holder.held.load()) std::this_thread::yield(); + Require(acquire() == 0); + Require(holder.releasing.load()); + Require(scePthreadJoin(holder.thread, nullptr) == SCE_OK); +} + +static void CheckRwlock() { + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr) == 0 && rwlock != nullptr); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, nullptr) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + RequireWaitsForHolder(&rwlock, false, [&] { return pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr); }); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + RequireWaitsForHolder(&rwlock, false, [&] { return pthread_rwlock_timedwrlock_nid_postfix(&rwlock, nullptr); }); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); +} + +static void CheckMutex() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, nullptr) == GUEST_EDEADLK); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + RequireWaitsForHolder(&mutex, true, [&] { return pthread_mutex_timedlock_nid_postfix(&mutex, nullptr); }); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); +} + +int main() { + CheckRwlock(); + CheckMutex(); +} diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index c1b6d0a298..1f882c248d 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -613,7 +613,8 @@ int main() { leaving.started.store(false); std::this_thread::sleep_for(std::chrono::microseconds(200)); Require(sceKernelSignalSema(leaving.sem, 1) == 0); - for (int spin = 0; spin < round % 16 * 64; ++spin) std::this_thread::yield(); + const auto offset = std::chrono::steady_clock::now() + std::chrono::microseconds(round % 16 * 8); + while (std::chrono::steady_clock::now() < offset) {} Require(sceKernelRaiseException(leavingThread, SIGUSR1) == 0); ExpectDelivery(1 + 2 * Repeats + HostRounds + round, leaving.id); leavingRound.store(round + 1); diff --git a/core/libs/tests/GuestRandom.cpp b/core/libs/tests/GuestRandom.cpp index f611927f88..df1d5f83c9 100644 --- a/core/libs/tests/GuestRandom.cpp +++ b/core/libs/tests/GuestRandom.cpp @@ -11,11 +11,13 @@ int main() { constexpr int invalid = static_cast(0x817C0016); unsigned char buffer[80]; Require(sceRandomGetRandomNumber(nullptr, 16) == invalid); + Require(sceRandomGetRandomNumber(nullptr, 0) == invalid); std::memset(buffer, 0xAA, sizeof(buffer)); Require(sceRandomGetRandomNumber(buffer, 65) == invalid); Require(buffer[0] == 0xAA); Require(sceRandomGetRandomNumber(buffer, 0) == 0); Require(buffer[0] == 0xAA); + Require(sceRandomGetRandomNumber(buffer, 1) == 0); Require(sceRandomGetRandomNumber(buffer, 7) == 0); Require(buffer[7] == 0xAA); unsigned char first[64] = {}; diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 85f04438fc..19ac107f19 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -8,6 +8,7 @@ int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); int APS5_VABI sceRazorCpuPushMarker(const char* label, std::uint32_t color, std::uint32_t flags); +int APS5_VABI sceRazorCpuNamedSync(const char* label); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); @@ -27,6 +28,7 @@ int main() { Require(sceRazorCpuJobManagerDispatch(nullptr) == 0); Require(sceRazorCpuJobManagerJob(nullptr) == 0); Require(sceRazorCpuJobManagerSequence(nullptr) == 0); + Require(sceRazorCpuNamedSync("frame") == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); diff --git a/core/libs/tests/GuestRecordLockOwners.cpp b/core/libs/tests/GuestRecordLockOwners.cpp new file mode 100644 index 0000000000..cbffd82062 --- /dev/null +++ b/core/libs/tests/GuestRecordLockOwners.cpp @@ -0,0 +1,148 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.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); +int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "record lock owner check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GetLock = 11; +constexpr int SetLock = 12; +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 int GuestEagain = 35; + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; +static_assert(sizeof(GuestLock) == 32); + +const char* const FileName = "owners.bin"; + +pid_t HoldLocks(int ready[2], int release[2]) { + const pid_t child = ::fork(); + Check(child >= 0); + if (child != 0) { + ::close(ready[1]); + ::close(release[0]); + return child; + } + ::close(ready[0]); + ::close(release[1]); + const int fd = ::open(FileName, O_RDWR); + struct flock read{}; + read.l_type = F_RDLCK; + read.l_whence = SEEK_SET; + read.l_start = 0; + read.l_len = 10; + struct flock write = read; + write.l_type = F_WRLCK; + write.l_start = 20; + write.l_len = 10; + struct flock tail = write; + tail.l_start = 50; + tail.l_len = 0; + const bool held = fd >= 0 && ::fcntl(fd, F_SETLK, &read) == 0 && ::fcntl(fd, F_SETLK, &write) == 0 && ::fcntl(fd, F_SETLK, &tail) == 0; + const char state = held ? 'y' : 'n'; + if (::write(ready[1], &state, 1) != 1) ::_exit(2); + char done = 0; + if (::read(release[0], &done, 1) != 1) ::_exit(3); + ::_exit(held ? 0 : 1); +} + +GuestLock Query(int fd, GuestLock request) { + Check(fcntl_nid_postfix(fd, GetLock, &request) == 0); + return request; +} + +bool Unchanged(const GuestLock& reply, const GuestLock& asked) { + return reply.type == Unlock && reply.start == asked.start && reply.length == asked.length && reply.pid == asked.pid && + reply.whence == asked.whence && reply.system == asked.system; +} + +bool Reports(const GuestLock& reply, std::int16_t type, std::int64_t start, std::int64_t length, pid_t owner) { + return reply.type == type && reply.start == start && reply.length == length && reply.pid == owner && reply.whence == FromStart; +} + +void OtherOwner() { + int ready[2]; + int release[2]; + Check(::pipe(ready) == 0 && ::pipe(release) == 0); + const pid_t child = HoldLocks(ready, release); + char state = 0; + Check(::read(ready[0], &state, 1) == 1 && state == 'y'); + + const int fd = sceKernelOpen(FileName, SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + + const GuestLock readRange{0, 10, 7, Unlock, FromStart, 3}; + Check(Unchanged(Query(fd, readRange), readRange)); + Check(Reports(Query(fd, {0, 0, 0, Unlock, FromStart, 0}), WriteLock, 20, 10, child)); + Check(Reports(Query(fd, {25, 1, 0, Unlock, FromStart, 0}), WriteLock, 20, 10, child)); + Check(Reports(Query(fd, {60, 5, 0, Unlock, FromStart, 0}), WriteLock, 50, 0, child)); + const GuestLock between{10, 10, 0, Unlock, FromStart, 0}; + Check(Unchanged(Query(fd, between), between)); + + const GuestLock sharedRead{0, 15, 0, ReadLock, FromStart, 0}; + Check(Unchanged(Query(fd, sharedRead), sharedRead)); + Check(Reports(Query(fd, {0, 15, 0, WriteLock, FromStart, 0}), ReadLock, 0, 10, child)); + Check(Reports(Query(fd, {22, 2, 0, ReadLock, FromStart, 0}), WriteLock, 20, 10, child)); + + GuestLock blocked{5, 1, 0, WriteLock, FromStart, 0}; + *__error_nid_postfix() = 0; + Check(fcntl_nid_postfix(fd, SetLock, &blocked) == -1 && *__error_nid_postfix() == GuestEagain); + + const char done = 'd'; + Check(::write(release[1], &done, 1) == 1); + int status = 0; + Check(::waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0); + + const GuestLock afterExit{0, 0, 0, Unlock, FromStart, 0}; + Check(Unchanged(Query(fd, afterExit), afterExit)); + Check(sceKernelClose(fd) == 0); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-record-lock-owners-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + { std::ofstream file(FileName, std::ios::binary); file << std::string(100, 'x'); } + OtherOwner(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestRefusedConnect.cpp b/core/libs/tests/GuestRefusedConnect.cpp new file mode 100644 index 0000000000..409f8ad97c --- /dev/null +++ b/core/libs/tests/GuestRefusedConnect.cpp @@ -0,0 +1,108 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +namespace { + +constexpr int GuestInet = 2; +constexpr int GuestStream = 1; +constexpr int GuestConnectionRefused = 61; +constexpr auto RefusalLimit = std::chrono::milliseconds(1000); + +void Require(bool value) { if (!value) std::abort(); } + +std::array ClosedLoopbackPort() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + Require(bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &length) == 0); + Require(close_nid_postfix(descriptor) == 0); + Require(address[2] != 0 || address[3] != 0); + return address; +} + +template +bool ConnectsOnceBacklogDrains(TConnect connect) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(listener >= 0); + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &length) == 0); + Require(listen_nid_postfix(listener, 1) == 0); + const int queued = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(queued >= 0 && connect_nid_postfix(queued, address.data(), address.size()) == 0); + std::thread acceptor([listener] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0 && close_nid_postfix(accepted) == 0); + }); + const bool connected = connect(address); + acceptor.join(); + Require(close_nid_postfix(queued) == 0 && close_nid_postfix(listener) == 0); + return connected; +} + +template +bool RefusedAtOnce(TConnect connect) { + const auto start = std::chrono::steady_clock::now(); + const bool refused = connect(); + return refused && std::chrono::steady_clock::now() - start < RefusalLimit; +} + +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + const auto address = ClosedLoopbackPort(); + Require(RefusedAtOnce([&] { + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool refused = connect_nid_postfix(descriptor, address.data(), address.size()) == -1 && *__error_nid_postfix() == GuestConnectionRefused; + Require(close_nid_postfix(descriptor) == 0); + return refused; + })); + Require(RefusedAtOnce([&] { + const int descriptor = sceNetSocket("refused", GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool refused = sceNetConnect(descriptor, address.data(), address.size()) == static_cast(0x80410100u | GuestConnectionRefused) + && *sceNetErrnoLoc() == GuestConnectionRefused; + Require(sceNetSocketClose(descriptor) == 0); + return refused; + })); + Require(ConnectsOnceBacklogDrains([](const std::array& listening) { + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool connected = connect_nid_postfix(descriptor, listening.data(), listening.size()) == 0; + Require(close_nid_postfix(descriptor) == 0); + return connected; + })); + Require(ConnectsOnceBacklogDrains([](const std::array& listening) { + const int descriptor = sceNetSocket("backlog", GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool connected = sceNetConnect(descriptor, listening.data(), listening.size()) == 0; + Require(sceNetSocketClose(descriptor) == 0); + return connected; + })); +} diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 96273f2b73..0cfa49baa9 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -17,6 +17,8 @@ int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentClock(RtcDateTime*, int); +int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime*); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick*, RtcTick*); int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); @@ -71,8 +73,12 @@ int main() { Require(sceRtcGetTickResolution() == 1000000); Require(sceRtcIsLeapYear(2000) == 1 && sceRtcIsLeapYear(1900) == 0 && sceRtcIsLeapYear(2024) == 1); Require(sceRtcIsLeapYear(0) == invalidYear); + Require(sceRtcIsLeapYear(10000) == 1); + Require(sceRtcIsLeapYear(9999) == 0 && sceRtcIsLeapYear(9996) == 1); Require(sceRtcGetDaysInMonth(2023, 2) == 28 && sceRtcGetDaysInMonth(2024, 2) == 29 && sceRtcGetDaysInMonth(2024, 4) == 30); Require(sceRtcGetDaysInMonth(2024, 13) == invalidMonth); + Require(sceRtcGetDaysInMonth(10000, 1) == 31); + Require(sceRtcGetDaysInMonth(9999, 1) == 31); Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); @@ -98,6 +104,13 @@ int main() { tick.tick = maxTick + 1; Require(sceRtcSetTick(&converted, &tick) == invalidValue); + Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); + Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClock(&converted, 60) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClock(&converted, -300) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClockLocalTime(nullptr) == invalidPointer); + Require(sceRtcGetCurrentClockLocalTime(&converted) == 0 && sceRtcCheckValid(&converted) == 0); + RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; std::int64_t seconds = -1; Require(sceRtcGetTime_t(&epoch, &seconds) == 0 && seconds == 0); diff --git a/core/libs/tests/GuestRudpContextLifecycle.cpp b/core/libs/tests/GuestRudpContextLifecycle.cpp new file mode 100644 index 0000000000..f043d30a99 --- /dev/null +++ b/core/libs/tests/GuestRudpContextLifecycle.cpp @@ -0,0 +1,113 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpActivate(); +int APS5_VABI sceRudpEnableInternalIOThread(std::uint32_t, std::uint32_t); +int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler, void*); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpBind(int, std::uint16_t, int, std::uint16_t); +int APS5_VABI sceRudpSetOption(int, int, const void*, std::uint32_t); +int APS5_VABI sceRudpInitiate(int, const void*, std::uint32_t, std::uint16_t); +int APS5_VABI sceRudpGetContextStatus(int, void*, std::uint32_t); +int APS5_VABI sceRudpWrite(int, const void*, std::uint32_t, int); +int APS5_VABI sceRudpRead(int, void*, std::uint32_t, int, void*); +int APS5_VABI sceRudpTerminate(int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContextId = static_cast(0x80770003u); + constexpr int connRefused = static_cast(0x8077000Au); + constexpr int connReset = static_cast(0x80770009u); + alignas(16) static unsigned char pool[0x10000]; + + Require(sceRudpActivate() == 0); + + Require(sceRudpEnableInternalIOThread(0, 0) == notInitialized); + + Require(sceRudpSetEventHandler(Handler, nullptr) == 0); + + Require(sceRudpBind(0, 0, 0, 0) == notInitialized); + Require(sceRudpBind(12345, 0, 0, 0) == notInitialized); + + Require(sceRudpSetOption(0, 0, nullptr, 0) == notInitialized); + Require(sceRudpSetOption(12345, 0, nullptr, 0) == notInitialized); + + Require(sceRudpInitiate(0, nullptr, 0, 0) == notInitialized); + Require(sceRudpInitiate(12345, nullptr, 0, 0) == notInitialized); + + { + unsigned char status[64] = {}; + Require(sceRudpGetContextStatus(0, status, sizeof(status)) == notInitialized); + Require(sceRudpGetContextStatus(12345, status, sizeof(status)) == notInitialized); + } + + Require(sceRudpWrite(0, nullptr, 0, 0) == notInitialized); + Require(sceRudpWrite(12345, nullptr, 0, 0) == notInitialized); + + Require(sceRudpRead(0, nullptr, 0, 0, nullptr) == notInitialized); + Require(sceRudpRead(12345, nullptr, 0, 0, nullptr) == notInitialized); + + Require(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + + Require(sceRudpActivate() == 0); + + Require(sceRudpEnableInternalIOThread(0, 0) == 0); + Require(sceRudpEnableInternalIOThread(1024, 1) == 0); + + Require(sceRudpBind(12345, 0, 0, 0) == invalidContextId); + + Require(sceRudpSetOption(12345, 0, nullptr, 0) == invalidContextId); + + Require(sceRudpInitiate(12345, nullptr, 0, 0) == invalidContextId); + + { + unsigned char status[64] = {}; + Require(sceRudpGetContextStatus(12345, status, sizeof(status)) == invalidContextId); + } + + Require(sceRudpWrite(12345, nullptr, 0, 0) == invalidContextId); + + Require(sceRudpRead(12345, nullptr, 0, 0, nullptr) == invalidContextId); + + int ctx = -1; + Require(sceRudpCreateContext(Handler, nullptr, &ctx) == 0); + Require(ctx >= 0); + + Require(sceRudpBind(ctx, 0, 0, 0) == 0); + + Require(sceRudpSetOption(ctx, 0, nullptr, 0) == 0); + + Require(sceRudpInitiate(ctx, nullptr, 0, 0) == connRefused); + + { + unsigned char status[64]; + std::memset(status, 0x5a, sizeof(status)); + Require(sceRudpGetContextStatus(ctx, status, sizeof(status)) == 0); + for (std::size_t i = 0; i < sizeof(status); ++i) Require(status[i] == 0); + } + Require(sceRudpGetContextStatus(ctx, nullptr, 64) == 0); + + Require(sceRudpWrite(ctx, nullptr, 0, 0) == connReset); + + Require(sceRudpRead(ctx, nullptr, 0, 0, nullptr) == connReset); + + Require(sceRudpSetEventHandler(Handler, nullptr) == 0); + + Require(sceRudpTerminate(ctx) == 0); + Require(sceRudpEnd() == 0); + return 0; +} \ No newline at end of file diff --git a/core/libs/tests/GuestSafeErrors.cpp b/core/libs/tests/GuestSafeErrors.cpp new file mode 100644 index 0000000000..436ce48e09 --- /dev/null +++ b/core/libs/tests/GuestSafeErrors.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI strerror_s_nid_postfix(char*, std::size_t, int); +std::size_t APS5_VABI strerrorlen_s_nid_postfix(int); +int APS5_VABI strerror_r_nid_postfix(int, char*, std::size_t); +int* APS5_VABI __error_nid_postfix(); +void* APS5_VABI set_constraint_handler_s_nid_postfix(void*); +void APS5_VABI ignore_handler_s_nid_postfix(const char*, void*, int); +} + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "safe error messages: %s\n", message); + std::abort(); + } +} + +thread_local int violations = 0; +thread_local void* violationBuffer = nullptr; + +void APS5_VABI Constraint(const char* message, void* buffer, int error) { + Require(message && std::strcmp(message, "strerror_s") == 0, "constraint callback message and ABI"); + Require(error == 22, "constraint callback error"); + violationBuffer = buffer; + ++violations; + auto previous = set_constraint_handler_s_nid_postfix(reinterpret_cast(Constraint)); + Require(previous == reinterpret_cast(Constraint), "handler is shared and callable without its lock"); + char nested[64]; + Require(strerror_s_nid_postfix(nested, sizeof(nested), 2) == 0, "handler can reenter valid conversion"); +} + +void CheckMessage(int error, const char* expected) { + const std::string full(expected); + *__error_nid_postfix() = 1234; + Require(strerrorlen_s_nid_postfix(error) == full.size(), "full message length excludes null"); + for (std::size_t capacity = 1; capacity <= full.size() + 2; ++capacity) { + std::array buffer; + buffer.fill('#'); + const auto result = strerror_s_nid_postfix(buffer.data() + 1, capacity, error); + std::string wanted = full; + if (capacity <= full.size()) { + wanted.resize(capacity - 1); + if (capacity > 3) wanted.replace(capacity - 4, 3, "..."); + } + Require(result == (capacity <= full.size() ? 34 : 0), "complete versus truncated return"); + Require(std::strcmp(buffer.data() + 1, wanted.c_str()) == 0, "truncated text, ellipsis and terminator"); + Require(buffer[0] == '#', "leading canary"); + for (std::size_t index = wanted.size() + 2; index < buffer.size(); ++index) + Require(buffer[index] == '#', "bytes after the terminator remain untouched"); + Require(*__error_nid_postfix() == 1234, "preserve guest errno"); + } +} + +void CheckMessages() { + CheckMessage(0, "No error: 0"); + CheckMessage(2, "No such file or directory"); + CheckMessage(11, "Resource deadlock avoided"); + CheckMessage(35, "Resource temporarily unavailable"); + CheckMessage(96, "Previous owner died"); + CheckMessage(-1, "Unknown error: -1"); + CheckMessage(97, "Unknown error: 97"); + CheckMessage(std::numeric_limits::min(), "Unknown error: -2147483648"); + CheckMessage(std::numeric_limits::max(), "Unknown error: 2147483647"); + for (int error = 0; error < 97; ++error) { + char expected[128]; + Require(strerror_r_nid_postfix(error, expected, sizeof(expected)) == 0, "existing guest error table"); + CheckMessage(error, expected); + } + Require(violations == 0, "truncation is not a constraint violation"); +} + +void CheckConstraints() { + std::array buffer; + buffer.fill('!'); + constexpr auto maximum = std::numeric_limits::max(); + for (const auto capacity : {std::size_t{0}, (maximum >> 1) + 1, maximum}) { + const int before = violations; + Require(strerror_s_nid_postfix(buffer.data(), capacity, 2) == 22, "reject invalid capacity"); + Require(violations == before + 1 && violationBuffer == buffer.data(), "one callback with destination"); + for (const char value : buffer) Require(value == '!', "constraint failure leaves destination untouched"); + } + for (const auto capacity : {std::size_t{0}, std::size_t{1}, maximum}) { + const int before = violations; + Require(strerror_s_nid_postfix(nullptr, capacity, 2) == 22, "reject null destination"); + Require(violations == before + 1 && violationBuffer == nullptr, "one callback for null destination"); + } + set_constraint_handler_s_nid_postfix(reinterpret_cast(ignore_handler_s_nid_postfix)); + Require(strerror_s_nid_postfix(nullptr, 1, 2) == 22, "installed ignore handler returns to caller"); + set_constraint_handler_s_nid_postfix(reinterpret_cast(Constraint)); +} + +void CheckThreads() { + const auto worker = [](int error, const char* message) { + for (int iteration = 0; iteration < 100; ++iteration) { + CheckMessage(error, message); + Require(strerror_s_nid_postfix(nullptr, 1, error) == 22, "thread constraint callback"); + } + Require(violations == 100, "callbacks execute in the calling thread"); + }; + std::thread first(worker, 35, "Resource temporarily unavailable"); + std::thread second(worker, -1, "Unknown error: -1"); + first.join(); + second.join(); +} + +} + +int main() { + auto previous = set_constraint_handler_s_nid_postfix(reinterpret_cast(Constraint)); + CheckMessages(); + CheckConstraints(); + CheckThreads(); + set_constraint_handler_s_nid_postfix(previous); + std::puts("safe error message checks passed"); +} diff --git a/core/libs/tests/GuestSandboxCertificateTrust.cpp b/core/libs/tests/GuestSandboxCertificateTrust.cpp new file mode 100644 index 0000000000..c2fdb60f40 --- /dev/null +++ b/core/libs/tests/GuestSandboxCertificateTrust.cpp @@ -0,0 +1,63 @@ +#include "prx/libkernel/File/include/TrustedRoots.hpp" + +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Certificate trust check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr char Certificate[] = R"(-----BEGIN CERTIFICATE----- +MIIBijCCATGgAwIBAgIUFMQYJ1in3pTJaCqjg6JVgSBNQgkwCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQQW55UFM1LVRlc3QtUm9vdDAeFw0yNjEwMTMwOTE2NTRaFw0z +NjEwMTAwOTE2NTRaMBsxGTAXBgNVBAMMEEFueVBTNS1UZXN0LVJvb3QwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAAQiHlYF9b/zaT5lFC26MOBndhdySsdLsuthNut0 +fhQkEHlybL5nUrvOPZrkV1e/vEZ2ykmq7xMkSYeIHnNy4wxMo1MwUTAdBgNVHQ4E +FgQUJKW6nreyN5yBQn0axEJkZnkfZB4wHwYDVR0jBBgwFoAUJKW6nreyN5yBQn0a +xEJkZnkfZB4wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiBfTER0 +UmcQ0u8NzHTODNgVOHZHQFrYjcOyVjzZlSKomQIgDhztVM5obBElAnm22JgWEPk+ +Bk30RAcx5xk3rjar3qs= +-----END CERTIFICATE----- +)"; + +static void CheckWindowsTrust() { + DWORD size = 0; + Require(CryptStringToBinaryA(Certificate, sizeof(Certificate) - 1, CRYPT_STRING_BASE64HEADER, nullptr, &size, nullptr, nullptr)); + std::vector der(size); + Require(CryptStringToBinaryA(Certificate, sizeof(Certificate) - 1, CRYPT_STRING_BASE64HEADER, der.data(), &size, nullptr, nullptr)); + const auto roots = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr); + const auto disallowed = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr); + Require(roots && disallowed); + PCCERT_CONTEXT certificate = nullptr; + Require(CertAddEncodedCertificateToStore(roots, X509_ASN_ENCODING, der.data(), size, CERT_STORE_ADD_NEW, &certificate)); + const auto unrestricted = ExportHostRoots_nid_no_patch(roots, disallowed); + Require(!unrestricted.empty() && unrestricted.find('\0') == std::string::npos); + DWORD decodedSize = size; + std::vector decoded(decodedSize); + Require(CryptStringToBinaryA(unrestricted.c_str(), static_cast(unrestricted.size()), CRYPT_STRING_BASE64HEADER, + decoded.data(), &decodedSize, nullptr, nullptr)); + Require(decoded == der); + CERT_ENHKEY_USAGE usage{}; + Require(CertSetEnhancedKeyUsage(certificate, &usage)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed).empty()); + char* email = const_cast(szOID_PKIX_KP_EMAIL_PROTECTION); + usage = {1, &email}; + Require(CertSetEnhancedKeyUsage(certificate, &usage)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed).empty()); + char* server = const_cast(szOID_PKIX_KP_SERVER_AUTH); + usage = {1, &server}; + Require(CertSetEnhancedKeyUsage(certificate, &usage)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed) == unrestricted); + Require(CertAddCertificateContextToStore(disallowed, certificate, CERT_STORE_ADD_NEW, nullptr)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed).empty()); + Require(CertFreeCertificateContext(certificate)); + Require(CertCloseStore(disallowed, CERT_CLOSE_STORE_CHECK_FLAG)); + Require(CertCloseStore(roots, CERT_CLOSE_STORE_CHECK_FLAG)); +} + +int main() { CheckWindowsTrust(); } diff --git a/core/libs/tests/GuestSandboxCertificates.cpp b/core/libs/tests/GuestSandboxCertificates.cpp new file mode 100644 index 0000000000..c95eded2f8 --- /dev/null +++ b/core/libs/tests/GuestSandboxCertificates.cpp @@ -0,0 +1,102 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +extern "C" { +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); +int APS5_VABI sceKernelOpen(const char*, int, unsigned short); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI sceKernelStat(const char*, FileStat*); +void* APS5_VABI fopen_nid_postfix(const char*, const char*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, void*); +int APS5_VABI fclose_nid_postfix(void*); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Sandbox certificates check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +static std::string ReadKernel(const std::string& path) { + const int descriptor = sceKernelOpen(path.c_str(), 0, 0); + Require(descriptor >= 0); + std::string text; + char buffer[4096]; + for (std::int64_t count; (count = sceKernelRead(descriptor, buffer, sizeof(buffer))) != 0;) { + Require(count > 0); + text.append(buffer, static_cast(count)); + } + Require(sceKernelClose(descriptor) == 0); + return text; +} +static std::string ReadStream(const std::string& path) { + void* stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = fread_nid_postfix(buffer, 1, sizeof(buffer), stream)) != 0;) text.append(buffer, count); + Require(fclose_nid_postfix(stream) == 0); + return text; +} +static void CheckOverrides(const std::string& directory, const std::string& path) { + const auto root = std::filesystem::current_path(); + const auto supplied = root / std::filesystem::path(path).relative_path(); + std::filesystem::create_directories(supplied.parent_path()); + const std::string expected = "explicit guest certificate bundle\n"; + { std::ofstream file(supplied, std::ios::binary | std::ios::trunc); file << expected; Require(static_cast(file)); } + Require(ReadKernel(path) == expected); + Require(ReadStream(path) == expected); + Require(ResolvePath_nid_no_patch(path.c_str()) == supplied); + Require(std::filesystem::remove(supplied)); + for (auto parent = supplied.parent_path(); parent != root; parent = parent.parent_path()) + Require(std::filesystem::remove(parent)); + const auto alias = root / "sandbox_certificate_override"; + std::filesystem::create_directory(alias); + { std::ofstream file(alias / "CA_LIST.cer", std::ios::binary | std::ios::trunc); file << expected; Require(static_cast(file)); } + AddPathAlias_nid_no_patch(directory.c_str(), alias.string().c_str()); + Require(ReadKernel(path) == expected && ReadStream(path) == expected); + BlockPathAlias_nid_no_patch(directory.c_str()); + Require(sceKernelOpen(path.c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(fopen_nid_postfix(path.c_str(), "rb") == nullptr); + AddPathAlias_nid_no_patch(directory.c_str(), alias.string().c_str()); + RemovePathAlias_nid_no_patch(directory.c_str()); + Require(std::filesystem::remove(alias / "CA_LIST.cer")); + Require(std::filesystem::remove(alias)); +} +int main() { +#ifndef _WIN32 + const auto bundle = std::filesystem::absolute("host_bundle.pem"); + const std::string expected = "-----BEGIN CERTIFICATE-----\nMIIBAA==\n-----END CERTIFICATE-----\n"; + { std::ofstream file(bundle, std::ios::binary | std::ios::trunc); file << expected; } + Require(setenv("SSL_CERT_FILE", bundle.c_str(), 1) == 0); +#endif + const std::string directory = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert"; + const std::string path = directory + "/CA_LIST.cer"; + CheckOverrides(directory, path); + const auto text = ReadKernel(path); + const auto begin = text.find("-----BEGIN CERTIFICATE-----"); + Require(begin != std::string::npos); + Require(text.find("-----END CERTIFICATE-----", begin) != std::string::npos); +#ifndef _WIN32 + Require(text == expected); +#endif + Require(ReadStream(path) == text); + Require(ReadKernel(directory + "/./ca_list.cer") == text); + FileStat status{}; + Require(sceKernelStat(path.c_str(), &status) == 0); + Require(status.st_size == static_cast(text.size())); + const auto cached = std::filesystem::current_path() / "cert_cache" / "CA_LIST.cer"; + Require(std::filesystem::is_regular_file(cached)); + Require((std::filesystem::status(cached).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none); + Require(sceKernelOpen((directory + "/CA_LIST2.cer").c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelOpen((directory + "/CA_LIST.cer.bak").c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(fopen_nid_postfix((directory + "/CA_LIST2.cer").c_str(), "rb") == nullptr); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 5d8e063710..25b68bdcb5 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -34,7 +34,20 @@ static void RequireRunning() { } int main() { + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "initial status"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate before init"); + Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close before init"); + SaveDataDialogResult uninitResult{}; + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before init"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc before init"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value before init"); + Require(sceSaveDataDialogInitialize() == 0, "initialize"); + Require(sceSaveDataDialogInitialize() == SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED, "double init"); + Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close while initialized not running"); + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before open"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc while initialized not running"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value while initialized not running"); SaveDataDirName names[2]{{}, {"SDU1"}}; SaveDataDialogItems items{}; @@ -55,8 +68,11 @@ int main() { Require(sceSaveDataDialogProgressBarSetValue(0, 25) == 0, "set progress"); Require(sceSaveDataDialogProgressBarInc(0, 25) == 0, "increment progress"); RequireRunning(); + Require(sceSaveDataDialogProgressBarInc(0, 10) == 0, "inc progress while running"); Require(sceSaveDataDialogProgressBarSetValue(0, 100) == 0, "complete progress"); RequireRunning(); + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, + "get result while running"); Require(sceSaveDataDialogClose(nullptr) == 0, "close save progress"); Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "closing save progress must finish it"); @@ -69,10 +85,20 @@ int main() { "save result must preserve its mode, directory and caller context"); } + Require(sceSaveDataDialogOpen(¶m) == 0, "open save progress for terminate test"); + RequireRunning(); + Require(sceSaveDataDialogTerminate() == 0, "terminate while running"); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate after terminate"); + + Require(sceSaveDataDialogInitialize() == 0, "re-initialize"); param.mode = 1; Require(sceSaveDataDialogOpen(¶m) == 0, "open selection dialog"); Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "selection dialog must retain the existing automatic response"); Require(sceSaveDataDialogTerminate() == 0, "terminate"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc after terminate"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value after terminate"); } diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 9249ab3838..5e7786a5dd 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,8 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); +int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); +int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void*, std::size_t, std::size_t*); } namespace { @@ -18,6 +21,8 @@ namespace { constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotMounted = -2137063420; constexpr int SaveDataErrorBusy = -2137063421; constexpr int SaveDataErrorNotFound = -2137063416; @@ -29,6 +34,40 @@ void Check(bool condition, const std::string& what) { ++failures; } +void CheckUnmountedCalls() { + SaveDataMountPoint point{}; + std::memcpy(point.data, "test", sizeof("test")); + SaveDataMountInfo info{}; + char buf[128]{}; + std::size_t gotSize = 0; + Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); + Check(sceSaveDataGetMountInfo(&point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); + Check(sceSaveDataGetMountInfo(&point, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); + Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); + Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); +} + +void CheckMountInfoAndParam(const SaveDataMountPoint* point) { + SaveDataMountInfo info{}; + char buf[128]{}; + char smallBuf[64]{}; + std::size_t gotSize = 0; + SaveDataMountPoint unmounted{}; + std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); + Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); + Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetMountInfo(point, &info) == 0, "getMountInfo on mounted save succeeds"); + Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); + Check(sceSaveDataGetParam(point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); + Check(sceSaveDataGetParam(point, 1, smallBuf, sizeof(smallBuf), &gotSize) == SaveDataErrorParameter, "getParam small buf is PARAMETER"); + Check(sceSaveDataGetParam(&unmounted, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), &gotSize) == 0, "getParam on mounted save succeeds"); + Check(gotSize == sizeof(buf), "getParam reports expected size"); + Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), nullptr) == 0, "getParam with null got_size on mounted save succeeds"); +} + void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { SceSaveDataDirName dirName{}; std::memcpy(dirName.data, name, std::strlen(name) + 1); @@ -40,6 +79,7 @@ void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedS Check(code == 0, what + ": mounts"); if (code != 0) return; Check(result.mount_status == expectedStatus, what + ": status is " + std::to_string(result.mount_status) + ", expected " + std::to_string(expectedStatus)); + CheckMountInfoAndParam(&result.mount_point); Check(sceSaveDataUmount2(0, &result.mount_point) == 0, what + ": unmounts"); } @@ -63,6 +103,40 @@ void CheckFailedMountKeepsResult() { Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the kept mount point unmounts the save"); } +std::set Tree() { + std::set paths; + for (const auto& entry : std::filesystem::recursive_directory_iterator(std::filesystem::current_path())) paths.insert(entry.path()); + return paths; +} + +void CheckInvalidNameIsRejected(const char* data, std::size_t size, const std::string& label) { + for (const std::uint32_t mode : {MountReadWrite | MountCreate2, MountReadWrite | MountCreate, MountReadWrite}) { + SceSaveDataDirName dirName{}; + std::memcpy(dirName.data, data, size); + SaveDataMount3 mount{}; + mount.dir_name = &dirName; + mount.mount_mode = mode; + SaveDataMountResult result; + std::memset(&result, 0xCD, sizeof(result)); + const SaveDataMountResult before = result; + const std::string what = label + " with mount mode " + std::to_string(mode); + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorParameter, what + " is a parameter error"); + Check(std::memcmp(&result, &before, sizeof(result)) == 0, what + " leaves the result as it was"); + } +} + +void CheckInvalidNamesAreRejected() { + const auto before = Tree(); + for (const char* invalid : {"", ".", "..", "a/b", "a\\b", "c:d"}) { + CheckInvalidNameIsRejected(invalid, std::strlen(invalid) + 1, std::string("\"") + invalid + "\""); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + CheckInvalidNameIsRejected(unterminated, sizeof(unterminated), "an unterminated name"); + Check(Tree() == before, "rejected names create nothing"); + MountAndCheck("after", MountReadWrite | MountCreate2, 1u, "CREATE2 after the rejected names"); +} + } int main() { @@ -70,12 +144,14 @@ int main() { std::filesystem::create_directories(root); const auto previous = std::filesystem::current_path(); std::filesystem::current_path(root); + CheckUnmountedCalls(); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); MountAndCheck("made", MountReadWrite | MountCreate, 1u, "CREATE of a missing save"); MountAndCheck("made", MountReadWrite, 0u, "open of an existing save"); CheckFailedMountKeepsResult(); + CheckInvalidNamesAreRejected(); std::filesystem::current_path(previous); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/core/libs/tests/GuestScanCount.cpp b/core/libs/tests/GuestScanCount.cpp index f77e73b62e..1ddfe19674 100644 --- a/core/libs/tests/GuestScanCount.cpp +++ b/core/libs/tests/GuestScanCount.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include +#include #include #include #include @@ -7,6 +9,24 @@ #include extern "C" int APS5_VABI sscanf_s_nid_postfix(const char*, const char*, ...); +extern "C" int APS5_VABI vsscanf_s_nid_postfix(const char*, const char*, VaList*); + +static int APS5_VABI ScanList(const char* input, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vsscanf_s_nid_postfix(input, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} static int failures = 0; @@ -36,6 +56,25 @@ template static void CheckCount(const char* modifier, int count) { } int main() { + for (const auto scan : {sscanf_s_nid_postfix, ScanList}) { + Require(scan("7", "%d", static_cast(nullptr)) == EOF, "Null integer destination must return EOF"); + Require(scan("1.5", "%f", static_cast(nullptr)) == EOF, "Null real destination must return EOF"); + Require(scan("abc", "%s", static_cast(nullptr), 4u) == EOF, "Null string destination must return EOF"); + Require(scan("a", "%c", static_cast(nullptr), 1u) == EOF, "Null character destination must return EOF"); + Require(scan("abc", "%[a-z]", static_cast(nullptr), 4u) == EOF, "Null scanset destination must return EOF"); + Require(scan("", "%n", static_cast(nullptr)) == EOF, "Null count destination must return EOF"); + int first = -1; + int later = -1; + Require(scan("7 8 9", "%d%d%d", &first, static_cast(nullptr), &later) == EOF && first == 7 && later == -1, + "Null destination after an assignment must stop with EOF"); + first = -1; + Require(scan("7", "%d%n%d", &first, static_cast(nullptr), &later) == EOF && first == 7 && later == -1, + "Null count after an assignment must stop with EOF"); + first = -1; + Require(scan("x", "%d%d", &first, static_cast(nullptr)) == 0 && first == -1, + "Matching failure must stop before an unreachable null destination"); + Require(scan("7 8", "%*d%d", &first) == 1 && first == 8, "Suppressed conversions must not consume destinations"); + } for (const int count : {0, 3, 127}) { CheckCount("hh", count); CheckCount("h", count); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 06e4eb3fe7..e5b47cdeae 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -30,6 +30,8 @@ extern const std::uint8_t in6addr_any_nid_postfix[16]; extern const std::uint8_t in6addr_loopback_nid_postfix[16]; int APS5_VABI sceNetInit_nid_postfix(void); int APS5_VABI sceNetShowNetstatWithMemory(int); +int APS5_VABI sceNetShowNetstat(void); +int APS5_VABI sceNetShowIfconfig(void); int APS5_VABI sceNetSocket(const char*, int, int, int); int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); @@ -42,6 +44,7 @@ std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSocketAbort(int, int); int APS5_VABI sceNetShutdown(int, int); int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); @@ -210,6 +213,8 @@ static std::string CaptureNetstat(int pool) { } static void CheckNetstat() { + Require(sceNetShowNetstat() == 0); + Require(sceNetShowIfconfig() == 0); Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); const int pool = sceNetPoolCreate("netstat", 4096, 0); Require(pool > 0); @@ -264,6 +269,141 @@ static void CheckUnboundSocketName() { } } +static void CheckUnboundDatagramReceive() { + const int waiting = sceNetSocket("unbound-receive", 2, 2, 17); + const int peer = sceNetSocket("unbound-peer", 2, 2, 17); + Require(waiting >= 0 && peer >= 0); + const int timeout = 50000; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + char buffer[16]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + for (int call = 0; call < 3; ++call) { + const auto start = std::chrono::steady_clock::now(); + const std::int64_t result = call == 0 ? sceNetRecv(waiting, buffer, sizeof(buffer), 0) + : call == 1 ? sceNetRecvfrom(waiting, buffer, sizeof(buffer), 0, nullptr, nullptr) + : sceNetRecvmsg(waiting, &receive, 0); + Require(Failed(result, 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + } + const int on = 1; + const int off = 0; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1200, &on, sizeof(on)) == 0); + Require(Failed(sceNetRecvfrom(waiting, buffer, sizeof(buffer), 0, nullptr, nullptr), 35)); + Require(sceNetSetsockopt(waiting, 0xffff, 0x1200, &off, sizeof(off)) == 0); + const int longTimeout = 2000000; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1106, &longTimeout, sizeof(longTimeout)) == 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(peer, address.data(), &size) == 0); + auto reply = std::async(std::launch::async, [waiting] { + char answer[16]{}; + return sceNetRecvfrom(waiting, answer, sizeof(answer), 0, nullptr, nullptr) == 5 && std::strcmp(answer, "pong") == 0; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(reply.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + Require(sceNetSendto(waiting, "ping", 5, 0, address.data(), address.size()) == 5); + std::array source{}; + size = source.size(); + Require(sceNetRecvfrom(peer, buffer, sizeof(buffer), 0, source.data(), &size) == 5 && std::strcmp(buffer, "ping") == 0); + Require(sceNetSendto(peer, "pong", 5, 0, source.data(), size) == 5); + Require(reply.wait_for(std::chrono::seconds(1)) == std::future_status::ready && reply.get()); + Require(Failed(sceNetBind_nid_postfix(waiting, address.data(), address.size()), 22)); + for (const int descriptor : {waiting, peer}) Require(sceNetSocketClose(descriptor) == 0); +#ifdef _WIN32 + for (const bool abort : {true, false}) { + const int socket = sceNetSocket("unbound-abort", 2, 2, 17); + Require(socket >= 0); + auto waiter = std::async(std::launch::async, [socket] { + char ignored[4]; + return sceNetRecvfrom(socket, ignored, sizeof(ignored), 0, nullptr, nullptr); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(waiter.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + Require((abort ? sceNetSocketAbort(socket, 0) : sceNetSocketClose(socket)) == 0); + Require(waiter.wait_for(std::chrono::seconds(1)) == std::future_status::ready && waiter.get() < 0); + if (abort) Require(sceNetSocketClose(socket) == 0); + } +#endif +} + +static void CheckUnboundDatagramShutdown() { + const int socket = sceNetSocket("unbound-shutdown", 2, 2, 17); + Require(socket >= 0); + auto waiter = std::async(std::launch::async, [socket] { + char ignored[4]; + return sceNetRecvfrom(socket, ignored, sizeof(ignored), 0, nullptr, nullptr); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(waiter.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + sceNetShutdown(socket, 0); + Require(waiter.wait_for(std::chrono::seconds(1)) == std::future_status::ready && waiter.get() == 0); + char buffer[4]{}; + Require(sceNetRecv(socket, buffer, sizeof(buffer), 0) == 0); + std::array source{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 1, nullptr, 0, 0}; + Require(sceNetRecvmsg(socket, &receive, 0) == 0 && receive.name_length == 0); +#ifdef _WIN32 + std::uint32_t size = source.size(); + Require(sceNetRecvfrom(socket, buffer, sizeof(buffer), 0, source.data(), &size) == 0 && size == 0); +#endif + Require(sceNetSocketClose(socket) == 0); +#ifdef _WIN32 + const int empty = sceNetSocket("unbound-empty", 2, 2, 17); + Require(empty >= 0); + Require(sceNetRecv(empty, buffer, 0, 0) == 0); + Require(sceNetRecvfrom(empty, buffer, 0, 0, nullptr, nullptr) == 0); + Require(sceNetSocketClose(empty) == 0); +#endif +} + +static void CheckUnboundDatagramLaterAddress() { + for (const bool connect : {false, true}) { + const int waiting = sceNetSocket("unbound-later", 2, 2, 17); + const int peer = sceNetSocket("unbound-later-peer", 2, 2, 17); + Require(waiting >= 0 && peer >= 0); + std::array peerAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + std::uint32_t size = peerAddress.size(); + Require(sceNetGetsockname(peer, peerAddress.data(), &size) == 0); + auto reply = std::async(std::launch::async, [waiting] { + char answer[16]{}; + return sceNetRecv(waiting, answer, sizeof(answer), 0) == 5 && std::strcmp(answer, "pong") == 0; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(reply.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + std::array waitingAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require((connect ? sceNetConnect(waiting, peerAddress.data(), peerAddress.size()) + : sceNetBind_nid_postfix(waiting, waitingAddress.data(), waitingAddress.size())) == 0); + size = waitingAddress.size(); + Require(sceNetGetsockname(waiting, waitingAddress.data(), &size) == 0); + Require(sceNetSendto(peer, "pong", 5, 0, waitingAddress.data(), size) == 5); + Require(reply.wait_for(std::chrono::seconds(1)) == std::future_status::ready && reply.get()); + for (const int descriptor : {waiting, peer}) Require(sceNetSocketClose(descriptor) == 0); + } +} + +static void CheckUnboundDatagramTimeoutAfterAddress() { + const int waiting = sceNetSocket("unbound-timeout", 2, 2, 17); + Require(waiting >= 0); + const int timeout = 500000; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + auto bound = std::async(std::launch::async, [waiting] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + return sceNetBind_nid_postfix(waiting, address.data(), address.size()) == 0; + }); + char buffer[4]; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(waiting, buffer, sizeof(buffer), 0), 35)); + const auto elapsed = std::chrono::steady_clock::now() - start; + Require(bound.get()); + Require(elapsed >= std::chrono::milliseconds(450) && elapsed < std::chrono::milliseconds(750)); + Require(sceNetSocketClose(waiting) == 0); +} + static void CheckEmptyEpoll() { const int epoll = sceNetEpollCreate("empty-epoll", 0); Require(epoll >= 0); @@ -313,7 +453,7 @@ static void CheckDontWait() { std::uint32_t size = address.size(); Require(sceNetGetsockname(receiver, address.data(), &size) == 0); const int timeout = 2000000; - Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + Require(sceNetSetsockopt(receiver, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); char buffer[16]{}; NetIovec in[] = {{buffer, sizeof(buffer)}}; NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; @@ -347,8 +487,11 @@ static void CheckDontWait() { } static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { - const int timeout = 50000; + const KernelTimeval timeout{0, 50000}; Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + int microseconds = -1; + std::uint32_t length = sizeof(microseconds); + Require(sceNetGetsockopt(receiver, 0xffff, 0x1106, µseconds, &length) == 0 && microseconds == 50000); char buffer[8]{}; std::array source{}; std::uint32_t size = source.size(); @@ -360,7 +503,7 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array(timeout.tv_sec), static_cast(timeout.tv_usec), microseconds, expected); std::abort(); } } @@ -498,7 +644,18 @@ static void CheckKernelTimeouts() { Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)) == -1 && *__error_nid_postfix() == 22); RequireTimeout(descriptor, 0x1006, 1250000); RequireTimeout(descriptor, 0x1005, 500000); - Require(sceNetSetsockopt(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)) == 0); + Require(Failed(sceNetSetsockopt(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)), 22)); + Require(Failed(sceNetSetsockopt(descriptor, 0xffff, 0x1006, &invalid, sizeof(invalid)), 33)); + Require(Failed(sceNetSetsockopt(descriptor, 0xffff, 0x1005, &negative, sizeof(negative)), 33)); + RequireTimeout(descriptor, 0x1006, 1250000); + RequireTimeout(descriptor, 0x1005, 500000); + int seconds = -1; + std::uint32_t truncated = sizeof(seconds); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1006, &seconds, &truncated) == 0 && truncated == sizeof(seconds) && seconds == 1); + const KernelTimeval quarter{0, 250000}; + Require(sceNetSetsockopt(descriptor, 0xffff, 0x1006, &quarter, sizeof(quarter)) == 0); + RequireTimeout(descriptor, 0x1006, 250000); + Require(sceNetSetsockopt(descriptor, 0xffff, 0x1106, µseconds, sizeof(microseconds)) == 0); RequireTimeout(descriptor, 0x1006, microseconds); #ifndef _WIN32 std::array loopback{16, 2, 0, 0, 127, 0, 0, 1}; @@ -715,6 +872,28 @@ static void CheckBufferSizes() { Require(sceNetSocketClose(socket) == 0); } +static void CheckOnesBroadcast() { + const auto read = [](int socket) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 0x10000, &value, &size) == 0 && size == sizeof(value)); + return value; + }; + const int socket = sceNetSocket("ones-broadcast", 2, 2, 17); + const int other = sceNetSocket("ones-broadcast-other", 2, 2, 17); + Require(socket >= 0 && other >= 0); + Require(read(socket) == 0); + for (const int requested : {1, 0, 7}) { + Require(sceNetSetsockopt(socket, 0xffff, 0x10000, &requested, sizeof(requested)) == 0); + Require(read(socket) == requested); + } + Require(read(other) == 0); + const std::uint8_t truncated = 1; + Require(Failed(sceNetSetsockopt(socket, 0xffff, 0x10000, &truncated, sizeof(truncated)), 22)); + Require(read(socket) == 7); + for (const int descriptor : {socket, other}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckUnspecifiedIpv6() { std::array unspecified{}; Require(sceNetInetPton(28, "::", unspecified.data()) == 1); @@ -840,7 +1019,7 @@ static void CheckDatagramDisconnect() { Require(local[4] == 0 && local[5] == 0 && local[6] == 0 && local[7] == 0); #endif const int timeout = 2000000; - Require(sceNetSetsockopt(dissolved, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + Require(sceNetSetsockopt(dissolved, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); const char unrelated[] = "unrelated sender"; Require(sceNetSendto(other, unrelated, sizeof(unrelated), 0, dissolvedAddress.data(), dissolvedAddress.size()) == sizeof(unrelated)); char buffer[32]{}; @@ -874,6 +1053,10 @@ int main() { CheckEpollIdent(); CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); + CheckUnboundDatagramReceive(); + CheckUnboundDatagramShutdown(); + CheckUnboundDatagramLaterAddress(); + CheckUnboundDatagramTimeoutAfterAddress(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); @@ -922,6 +1105,7 @@ int main() { CheckDatagramAddressLengths(28); CheckStreamAddressLengths(); CheckBufferSizes(); + CheckOnesBroadcast(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index f5eddd3d84..466d204c8c 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -59,6 +59,19 @@ int main() { Require(!read.Has(reader)); } + { + const GuestTimeval limit{0, 1000}; + const auto start = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) { + GuestDescriptorSet read{}; + read.Add(reader); + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(!read.Has(reader)); + } + const auto elapsed = std::chrono::steady_clock::now() - start; + Require(elapsed >= std::chrono::milliseconds(20) && elapsed < std::chrono::milliseconds(200)); + } + { const char byte = 's'; Require(write_nid_postfix(writer, &byte, 1) == 1); diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index d2fe283b8a..016c4c5a0f 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -198,6 +198,23 @@ int main() { Require(received == 30); Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); Require(sigprocmask_nid_postfix(2, &user, nullptr) == 0); + const GuestSignalSet highBlock{{1u << 29, 1u << 7, 0, 0}}; + const GuestSignalSet suspendMask{{0, 0, 1u << 8, 0}}; + Require(sigprocmask_nid_postfix(3, &highBlock, nullptr) == 0); + Require(signal_nid_postfix(73, Callback) != invalid); + received = 0; + SuspendWait highWait{&suspendMask, 0, 0}; + void* highSuspender = nullptr; + Require(scePthreadCreate(&highSuspender, nullptr, SuspendEntry, &highWait, nullptr) == 0); + SuspendSender highSender{highSuspender, -1}; + void* highSenderThread = nullptr; + Require(scePthreadCreate(&highSenderThread, nullptr, SuspendSend, &highSender, nullptr) == 0); + Require(scePthreadJoin(highSenderThread, nullptr) == 0 && highSender.result == 0); + Require(scePthreadJoin(highSuspender, nullptr) == 0); + Require(highWait.result == -1 && highWait.error == 4 && received == 30); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && std::memcmp(&previous, &highBlock, sizeof(highBlock)) == 0); + Require(sigprocmask_nid_postfix(3, &empty, nullptr) == 0); + Require(signal_nid_postfix(73, nullptr) == Callback); signal_nid_postfix(30, nullptr); Require(GuestAlarm_nid_no_patch(0) == 0); @@ -275,4 +292,22 @@ int main() { 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); + + const GuestSignalSet highSignals{{0, 1u << 7, 1u << 8, 1u << 31}}; + for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, Callback) != invalid); + received = 0; + GuestSignalSet previousHighSignals{}; + Require(sigprocmask_nid_postfix(1, &highSignals, &previousHighSignals) == 0); + Require(previousHighSignals.bits[0] == 0 && previousHighSignals.bits[1] == 0 && + previousHighSignals.bits[2] == 0 && previousHighSignals.bits[3] == 0); + for (int guest : {40, 73, 128}) Require(raise_nid_postfix(guest) == 0 && received == 0); + Require(sigpending_nid_postfix(&pendingSignals) == 0 && + std::memcmp(&pendingSignals, &highSignals, sizeof(highSignals)) == 0); + Require(sigprocmask_nid_postfix(2, &highSignals, &previousHighSignals) == 0); + Require(std::memcmp(&previousHighSignals, &highSignals, sizeof(highSignals)) == 0); + Require(received == 128); + Require(sigpending_nid_postfix(&pendingSignals) == 0 && + pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && + pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, nullptr) == Callback); } diff --git a/core/libs/tests/GuestSocketConnectListening.cpp b/core/libs/tests/GuestSocketConnectListening.cpp new file mode 100644 index 0000000000..9bfffa3172 --- /dev/null +++ b/core/libs/tests/GuestSocketConnectListening.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Inet = 2; +constexpr int Stream = 1; +constexpr int ErrorNotSupported = 45; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +} + +int main() { + const int listener = socket_nid_postfix(Inet, Stream, 0); + Require(listener >= 0, "socket failed"); + std::array address{16, Inet, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0, "bind failed"); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &length) == 0 && length == address.size(), "getsockname failed"); + Require(listen_nid_postfix(listener, 4) == 0, "listen failed"); + + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, address.data(), address.size()) == -1 && *__error_nid_postfix() == ErrorNotSupported, + "connect on a listening socket must fail with EOPNOTSUPP (45)"); + + const int plain = socket_nid_postfix(Inet, Stream, 0); + Require(plain >= 0, "socket failed"); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(plain, nullptr, address.size()) == -1, "connect with a null address must fail"); + const int nullError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, nullptr, address.size()) == -1 && *__error_nid_postfix() == nullError, + "connect on a listening socket with a null address must fail as on any socket, not with EOPNOTSUPP"); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(plain, address.data(), 1) == -1, "connect with a 1-byte address must fail"); + const int shortError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, address.data(), 1) == -1 && *__error_nid_postfix() == shortError, + "connect on a listening socket with a 1-byte address must fail as on any socket, not with EOPNOTSUPP"); + Require(nullError != ErrorNotSupported && shortError != ErrorNotSupported, "address errors must not be EOPNOTSUPP"); + Require(close_nid_postfix(plain) == 0, "close failed"); + std::array wrongFamily{28, 28}; + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, wrongFamily.data(), wrongFamily.size()) == -1 && *__error_nid_postfix() == ErrorNotSupported, + "connect on a listening socket with an address of another family must fail with EOPNOTSUPP (45)"); + + const int client = socket_nid_postfix(Inet, Stream, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0, "a refused connect must leave the listener working"); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0, "accept failed"); + Require(close_nid_postfix(accepted) == 0 && close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close failed"); + return 0; +} diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index e8340085ae..76618bf409 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -181,7 +181,24 @@ static void DuplicateKeepsSocket() { Require(close_nid_postfix(sender) == 0); } +static void SocketTypeErrors() { + constexpr int PrototypeError = 41; + constexpr int ProtocolError = 43; + constexpr int FamilyError = 47; + constexpr int Inet6 = 28; + for (const int family : {Inet, Inet6}) { + Require(socket_nid_postfix(family, 99, 0) == -1 && *__error_nid_postfix() == PrototypeError); + Require(socket_nid_postfix(family, 4, 0) == -1 && *__error_nid_postfix() == PrototypeError); + Require(socket_nid_postfix(family, 0, 0) == -1 && *__error_nid_postfix() == ProtocolError); + Require(socket_nid_postfix(family, 99, 6) == -1 && *__error_nid_postfix() == ProtocolError); + Require(socket_nid_postfix(family, 1, 17) == -1 && *__error_nid_postfix() == ProtocolError); + Require(socket_nid_postfix(family, 2, 6) == -1 && *__error_nid_postfix() == ProtocolError); + } + Require(socket_nid_postfix(99, 1, 0) == -1 && *__error_nid_postfix() == FamilyError); +} + int main() { + SocketTypeErrors(); SelectSocketAndFile(); CloseReleasesNumber(); DuplicateKeepsSocket(); diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp new file mode 100644 index 0000000000..b475463949 --- /dev/null +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -0,0 +1,167 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct Iovec { + void* base; + std::uint64_t length; +}; + +struct Msghdr { + void* name; + std::uint32_t nameLength; + Iovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI recvmsg_nid_postfix(int, Msghdr*, int); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket timeout check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int SocketLevel = 0xffff; +constexpr int TcpLevel = 6; +constexpr int SendBuffer = 0x1001; +constexpr int ReceiveBuffer = 0x1002; +constexpr int SendTimeout = 0x1005; +constexpr int ReceiveTimeout = 0x1006; +constexpr int DontWait = 0x80; +constexpr int Invalid = 22; +constexpr int Domain = 33; +constexpr int WouldBlock = 35; +constexpr int NoProtocolOption = 42; + +using Clock = std::chrono::steady_clock; + +static bool Fails(std::int64_t result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static int Set(int descriptor, int option, std::int64_t seconds, std::int64_t microseconds) { + const Timeval time{seconds, microseconds}; + return setsockopt_nid_postfix(descriptor, SocketLevel, option, &time, sizeof(time)); +} + +static bool Reads(int descriptor, int option, std::int64_t seconds, std::int64_t microseconds) { + Timeval time{-1, -1}; + std::uint32_t length = sizeof(time); + return getsockopt_nid_postfix(descriptor, SocketLevel, option, &time, &length) == 0 && length == sizeof(time) && + time.seconds == seconds && time.microseconds == microseconds; +} + +int main() { + const int datagram = socket_nid_postfix(2, 2, 0); + std::array own{16, 2, 0, 0, 127, 0, 0, 1}; + Require(datagram >= 0 && bind_nid_postfix(datagram, own.data(), own.size()) == 0); + std::uint32_t ownSize = own.size(); + Require(getsockname_nid_postfix(datagram, own.data(), &ownSize) == 0); + + Require(Reads(datagram, ReceiveTimeout, 0, 0) && Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, ReceiveTimeout, 2, 0) == 0 && Reads(datagram, ReceiveTimeout, 2, 0)); + Require(Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, SendTimeout, 1, 500000) == 0 && Reads(datagram, SendTimeout, 1, 499999)); + Require(Set(datagram, SendTimeout, 0, 1) == 0 && Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, SendTimeout, std::int64_t{1} << 31, 7) == 0 && Reads(datagram, SendTimeout, 0x7fffffff, 999999)); + Require(Set(datagram, SendTimeout, 0, 0) == 0 && Reads(datagram, SendTimeout, 0, 0)); + Require(Fails(Set(datagram, ReceiveTimeout, -1, 0), Domain)); + Require(Fails(Set(datagram, ReceiveTimeout, 0, -1), Domain)); + Require(Fails(Set(datagram, ReceiveTimeout, 0, 1000000), Domain)); + Timeval time{0, 0}; + Require(Fails(setsockopt_nid_postfix(datagram, SocketLevel, ReceiveTimeout, &time, sizeof(time) - 1), Invalid)); + Require(Reads(datagram, ReceiveTimeout, 2, 0)); + std::int64_t seconds = -1; + std::uint32_t length = sizeof(seconds); + Require(getsockopt_nid_postfix(datagram, SocketLevel, ReceiveTimeout, &seconds, &length) == 0 && length == sizeof(seconds) && seconds == 2); + length = sizeof(time); + Require(Fails(getsockopt_nid_postfix(datagram, TcpLevel, ReceiveTimeout, &time, &length), Invalid)); + + char byte = 0; + Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); + auto started = Clock::now(); + Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), 0), WouldBlock)); + Require(Clock::now() - started >= std::chrono::milliseconds(59)); + Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), 0, nullptr, nullptr), WouldBlock)); + Iovec part{&byte, sizeof(byte)}; + Msghdr message{nullptr, 0, &part, 1, nullptr, 0, 0}; + Require(Fails(recvmsg_nid_postfix(datagram, &message, 0), WouldBlock)); + Require(Fails(read_nid_postfix(datagram, &byte, sizeof(byte)), WouldBlock)); +#ifndef _WIN32 + Require(Set(datagram, ReceiveTimeout, 60, 0) == 0); + Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), DontWait), WouldBlock)); + Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), DontWait, nullptr, nullptr), WouldBlock)); + Require(Fails(recvmsg_nid_postfix(datagram, &message, DontWait), WouldBlock)); + Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); +#endif + Require(sendto_nid_postfix(datagram, "d", 1, 0, own.data(), own.size()) == 1); + Require(recv_nid_postfix(datagram, &byte, sizeof(byte), 0) == 1 && byte == 'd'); + + const int listener = socket_nid_postfix(2, 1, 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(listener >= 0 && bind_nid_postfix(listener, address.data(), address.size()) == 0); + int small = 4096; + Require(setsockopt_nid_postfix(listener, SocketLevel, ReceiveBuffer, &small, sizeof(small)) == 0); + Require(Set(listener, ReceiveTimeout, 3, 0) == 0); + Require(listen_nid_postfix(listener, 4) == 0); + std::uint32_t addressSize = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &addressSize) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && setsockopt_nid_postfix(client, SocketLevel, SendBuffer, &small, sizeof(small)) == 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0 && Reads(server, ReceiveTimeout, 3, 0) && Reads(server, SendTimeout, 0, 0)); + + Require(Set(client, ReceiveTimeout, 0, 60000) == 0); + started = Clock::now(); + Require(Fails(recv_nid_postfix(client, &byte, sizeof(byte), 0), WouldBlock)); + Require(Clock::now() - started >= std::chrono::milliseconds(59)); + + Require(Set(client, SendTimeout, 0, 60000) == 0); + const std::vector chunk(1024, 'x'); + std::int64_t sent = 0; + bool stopped = false; + for (int round = 0; round < 200000 && !stopped; ++round) { + const auto result = send_nid_postfix(client, chunk.data(), chunk.size(), 0); + if (result > 0) sent += result; + else stopped = Fails(result, WouldBlock); + Require(result > 0 || stopped); + } + Require(stopped && sent > 0); + + Require(close_nid_postfix(server) == 0 && close_nid_postfix(client) == 0); + Require(close_nid_postfix(listener) == 0 && close_nid_postfix(datagram) == 0); + return 0; +} diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 1046e7ee55..efda5060a0 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -18,6 +18,11 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslSetMinSslVersion(int, int); +int APS5_VABI sceSslSetAlpn(int, const char*); +const char* APS5_VABI sceSslGetAlpnSelected(int); } struct SslMemoryPoolStats { @@ -103,6 +108,20 @@ int main() { Require(connection > 0 && connection != context); Require(sceSslSetVerifyCallback(connection, VerifyCallback, &marker) == 0); Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0); + constexpr std::uint32_t serverVerify = 0x1; + constexpr std::uint32_t cnCheck = 0x4; + Require(sceSslEnableVerifyOption(connection, serverVerify | cnCheck) == 0); + Require(sceSslDisableVerifyOption(connection, cnCheck) == 0); + Require(sceSslDisableVerifyOption(connection, serverVerify | cnCheck) == 0); + Require(sceSslSetMinSslVersion(connection, 0) == 0); + Require(sceSslSetMinSslVersion(connection, 5) == 0); + Require(Throws([&] { sceSslSetMinSslVersion(connection, -1); })); + Require(Throws([&] { sceSslSetMinSslVersion(connection, 6); })); + Require(sceSslGetAlpnSelected(connection) == nullptr); + Require(sceSslSetAlpn(connection, "http/1.1") == 0); + Require(sceSslSetAlpn(connection, "") == 0); + Require(Throws([&] { sceSslSetAlpn(connection, nullptr); })); + Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; Require(Throws([&] { sceSslConnect(connection); })); Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); @@ -110,10 +129,20 @@ int main() { const int unknown = connection + 1000; Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); + Require(Throws([&] { sceSslEnableVerifyOption(unknown, serverVerify); })); + Require(Throws([&] { sceSslDisableVerifyOption(unknown, serverVerify); })); + Require(Throws([&] { sceSslSetMinSslVersion(unknown, 5); })); + Require(Throws([&] { sceSslSetAlpn(unknown, "http/1.1"); })); + Require(Throws([&] { sceSslGetAlpnSelected(unknown); })); Require(Throws([&] { sceSslDeleteConnection(unknown); })); Require(sceSslTerm_nid_postfix(context) == 0); Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); + Require(Throws([&] { sceSslEnableVerifyOption(connection, serverVerify); })); + Require(Throws([&] { sceSslDisableVerifyOption(connection, serverVerify); })); + Require(Throws([&] { sceSslSetMinSslVersion(connection, 5); })); + Require(Throws([&] { sceSslSetAlpn(connection, "http/1.1"); })); + Require(Throws([&] { sceSslGetAlpnSelected(connection); })); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); Require(sceSslDeleteConnection(connection) == 0); Require(Throws([&] { sceSslDeleteConnection(connection); })); diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index aaa93f2962..8c26ab31c7 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -23,6 +23,12 @@ int APS5_VABI sceSslFreeCaCerts(int, SceSsl::CaCerts*); int APS5_VABI sceSslCreateConnection(int, int, const char*); int APS5_VABI sceSslDeleteConnection(int); int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +int APS5_VABI sceSslGetSerialNumber(int, void*, unsigned char*, std::size_t*); +void* APS5_VABI sceSslGetSubjectName(int, void*); +void* APS5_VABI sceSslGetIssuerName(int, void*); +int APS5_VABI sceSslGetNameEntryCount(int, void*); +int APS5_VABI sceSslGetNameEntryInfo(int, void*, int, char*, std::size_t, char*, std::size_t, std::size_t*); +int APS5_VABI sceSslFreeSslCertName(int, void*); } namespace { @@ -157,6 +163,102 @@ void ClientIdentity() { Check(sceSslTerm_nid_postfix(id) == 0, "identity term failed"); } +void CertificateReaders() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + auto data = Data(RootPem); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); + SceSsl::CaList handles{}; + Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); + void* cert = handles.certs[0]; + + std::size_t querySize = 0; + Check(sceSslGetSerialNumber(id, cert, nullptr, &querySize) == 0 && querySize == 3, "query serial size failed"); + querySize = 0; + unsigned char shortBuf[2]{}; + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 3, "zero-len serial query failed"); + querySize = sizeof(shortBuf); + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == static_cast(0x8095177a) && querySize == 3, + "short serial buffer accepted"); + unsigned char serialData[8]{}; + std::size_t serialLen = sizeof(serialData); + Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); + Check(serialLen == 3, "serial length mismatch"); + Check(serialData[0] == 0x00 && serialData[1] == 0x80 && serialData[2] == 0xa1, "serial bytes mismatch"); + Check(sceSslGetSerialNumber(id, cert, serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); + Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); + void* fake = &serialLen; + Throws([&] { sceSslGetSerialNumber(id, fake, serialData, &serialLen); }); + Throws([&] { sceSslGetSerialNumber(9999, cert, serialData, &serialLen); }); + + void* subject = sceSslGetSubjectName(id, cert); + Check(subject != nullptr, "get subject failed"); + void* issuer = sceSslGetIssuerName(id, cert); + Check(issuer != nullptr, "get issuer failed"); + Throws([&] { sceSslGetSubjectName(id, nullptr); }); + Throws([&] { sceSslGetIssuerName(id, nullptr); }); + Throws([&] { sceSslGetSubjectName(id, fake); }); + Throws([&] { sceSslGetIssuerName(id, fake); }); + Throws([&] { sceSslGetSubjectName(9999, cert); }); + Throws([&] { sceSslGetIssuerName(9999, cert); }); + + Check(sceSslGetNameEntryCount(id, subject) == 3, "subject entry count mismatch"); + Check(sceSslGetNameEntryCount(id, issuer) == 3, "issuer entry count mismatch"); + Check(sceSslGetNameEntryCount(id, nullptr) == static_cast(0x8095177a), "null cert name count accepted"); + Throws([&] { sceSslGetNameEntryCount(id, fake); }); + Throws([&] { sceSslGetNameEntryCount(9999, subject); }); + + char oid[32]{}; + char val[64]{}; + std::size_t vlen = 0; + Check(sceSslGetNameEntryInfo(id, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); + Check(std::strcmp(oid, "C") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); + Check(std::strcmp(oid, "O") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); + Check(std::strcmp(oid, "CN") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); + + char smallVal[4]{}; + std::size_t fullLen = 0; + Check(sceSslGetNameEntryInfo(id, subject, 1, nullptr, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); + Check(std::strcmp(smallVal, "Any") == 0 && fullLen == 12, "buffer truncation mismatch"); + + Check(sceSslGetNameEntryInfo(id, subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "negative index accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "out of bounds index accepted"); + Check(sceSslGetNameEntryInfo(id, nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "null certName accepted"); + Throws([&] { sceSslGetNameEntryInfo(id, fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + Throws([&] { sceSslGetNameEntryInfo(9999, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + + Check(sceSslFreeSslCertName(id, subject) == 0, "free subject name failed"); + Check(sceSslFreeSslCertName(id, issuer) == 0, "free issuer name failed"); + Check(sceSslFreeSslCertName(id, nullptr) == static_cast(0x8095177a), "null cert name free accepted"); + Throws([&] { sceSslFreeSslCertName(id, subject); }); + Throws([&] { sceSslFreeSslCertName(id, fake); }); + Throws([&] { sceSslFreeSslCertName(9999, subject); }); + + Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); + Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); + Throws([&] { sceSslGetSerialNumber(id, cert, serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(id, cert); }); + Throws([&] { sceSslGetIssuerName(id, cert); }); + + const int id2 = sceSslInit_nid_postfix(1024 * 1024); + Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); + SceSsl::CaList handles2{}; + Check(sceSslGetCaList(id2, &handles2) == 0, "get ca list failed"); + void* cert2 = handles2.certs[0]; + void* subject2 = sceSslGetSubjectName(id2, cert2); + Check(subject2 != nullptr, "get subject 2 failed"); + Check(sceSslFreeCaList(id2, &handles2) == 0, "free ca list failed"); + Check(sceSslUnloadCert(id2) == 0, "unload failed"); + Throws([&] { sceSslGetSerialNumber(id2, cert2, serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(id2, cert2); }); + Throws([&] { sceSslGetIssuerName(id2, cert2); }); + Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); + Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); + Throws([&] { sceSslFreeSslCertName(id2, subject2); }); +} + } int main() { @@ -165,6 +267,7 @@ int main() { AtomicFailures(); ClientIdentity(); ConnectionLifetime(); + CertificateReaders(); std::vector workers; for (int i = 0; i < 3; ++i) workers.emplace_back([] { for (int n = 0; n < 4; ++n) Lifecycle(); }); for (auto& worker : workers) worker.join(); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 83b97d5ecd..9e99c39e09 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,10 @@ extern FileStream* __stdoutp_nid_postfix; extern FileStream* __stderrp_nid_postfix; extern int __isthreaded_nid_postfix; int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fputs_nid_postfix(const char*, FileStream*); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI vprintf_nid_postfix(const char*, void*); +int APS5_VABI puts_nid_postfix(const char*); int APS5_VABI vfprintf_nid_postfix(FileStream*, const char*, void*); int APS5_VABI vsprintf_nid_postfix(char*, const char*, void*); int APS5_VABI fgetc_nid_postfix(FileStream*); @@ -42,6 +47,13 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream*); int APS5_VABI putc_unlocked_nid_postfix(int, FileStream*); int APS5_VABI fputwc_nid_postfix(char16_t, FileStream*); int APS5_VABI fputws_nid_postfix(const char16_t*, FileStream*); +std::int32_t APS5_VABI getwc_nid_postfix(FileStream*); +std::int32_t APS5_VABI getwchar_nid_postfix(); +int APS5_VABI putwc_nid_postfix(char16_t, FileStream*); +int APS5_VABI putwchar_nid_postfix(char16_t); +char16_t* APS5_VABI fgetws_nid_postfix(char16_t*, int, FileStream*); +int APS5_VABI getchar_unlocked_nid_postfix(); +int APS5_VABI putchar_unlocked_nid_postfix(int); int APS5_VABI fscanf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI __srget_nid_postfix(FileStream*); int APS5_VABI __swbuf_nid_postfix(int, FileStream*); @@ -80,6 +92,22 @@ static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) #endif return result; } +static int APS5_VABI PrintStandard(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vprintf_nid_postfix(format, args); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} static int APS5_VABI FormatString(char* buffer, const char* format, ...) { #ifdef _WIN32 __builtin_sysv_va_list args; @@ -493,7 +521,38 @@ static void CheckDescriptorAccessModes() { Require(std::filesystem::remove(filename)); } -int main() { +static void CheckClosedStreamWrites() { + FileStream stream(std::tmpfile()); + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", &stream) == nullptr); + Require(fputs_nid_postfix("text", &stream) == -1 && *__error_nid_postfix() == 9); + Require(fprintf_nid_postfix(&stream, "%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(fwrite_nid_postfix("text", 1, 4, &stream) == 0 && *__error_nid_postfix() == 9); + Require(fputc_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + Require(putc_unlocked_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + Require(fflush_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + char buffer[8] = {}; + Require(fread_nid_postfix(buffer, 1, sizeof(buffer), &stream) == 0 && *__error_nid_postfix() == 9); + Require(fgetc_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(fgets_nid_postfix(buffer, sizeof(buffer), &stream) == nullptr && *__error_nid_postfix() == 9); + Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == -1 && *__error_nid_postfix() == 9); + Require(ftello_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(fileno_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(ferror_nid_postfix(&stream) == 0 && feof_nid_postfix(&stream) == 0); + Require(fclose_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); +} + +static int CheckClosedStandardOutput() { + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", __stdoutp_nid_postfix) == nullptr); + Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); + Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + return 0; +} + +int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); + CheckClosedStreamWrites(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); CheckFlushWhileReading(); @@ -543,6 +602,25 @@ int main() { Require(fileno_nid_postfix(__stdinp_nid_postfix) == 0); Require(fileno_nid_postfix(__stdoutp_nid_postfix) == 1); Require(fileno_nid_postfix(__stderrp_nid_postfix) == 2); + auto* const savedInput = __stdinp_nid_postfix; + auto* const savedOutput = __stdoutp_nid_postfix; + FileStream customIn(std::tmpfile()); + Require(fputc_nid_postfix('T', &customIn) == 'T'); + std::rewind(customIn.GetHandle()); + __stdinp_nid_postfix = &customIn; + Require(getchar_unlocked_nid_postfix() == 'T'); + Require(getchar_unlocked_nid_postfix() == EOF); + Require(ungetc_nid_postfix('W', &customIn) == 'W'); + Require(getwchar_nid_postfix() == 'W'); + FileStream customOut(std::tmpfile()); + __stdoutp_nid_postfix = &customOut; + Require(putchar_unlocked_nid_postfix('U') == 'U'); + Require(putwchar_nid_postfix(u'V') == u'V'); + std::rewind(customOut.GetHandle()); + Require(fgetc_nid_postfix(&customOut) == 'U'); + Require(fgetc_nid_postfix(&customOut) == 'V'); + __stdinp_nid_postfix = savedInput; + __stdoutp_nid_postfix = savedOutput; FileStream stream(std::tmpfile()); auto& guest = *reinterpret_cast(&stream); Require(&guest == &stream.GuestState()); @@ -579,6 +657,62 @@ int main() { Require(std::memcmp(wideBytes, "AB\xC3\xA9", 4) == 0); wide.Close(); + FileStream wideIo(std::tmpfile()); + Require(putwc_nid_postfix(u'H', &wideIo) == u'H'); + Require(putwc_nid_postfix(u'i', &wideIo) == u'i'); + Require(putwc_nid_postfix(u'\n', &wideIo) == u'\n'); + Require(fputws_nid_postfix(u"Next\n", &wideIo) == 5); + Require(fputws_nid_postfix(u"End", &wideIo) == 3); + std::rewind(wideIo.GetHandle()); + Require(getwc_nid_postfix(&wideIo) == 'H'); + std::rewind(wideIo.GetHandle()); + char16_t wideBuf[16]{}; + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'H' && wideBuf[1] == u'i' && wideBuf[2] == u'\n' && wideBuf[3] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'N' && wideBuf[1] == u'e' && wideBuf[2] == u'x' && wideBuf[3] == u't' && wideBuf[4] == u'\n' && wideBuf[5] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'E' && wideBuf[1] == u'n' && wideBuf[2] == u'd' && wideBuf[3] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == nullptr); + Require(feof_nid_postfix(&wideIo)); + Require(fgetws_nid_postfix(wideBuf, 0, &wideIo) == nullptr && *__error_nid_postfix() == 22); + Require(ferror_nid_postfix(&wideIo)); + clearerr_nid_postfix(&wideIo); + Require(fgetws_nid_postfix(nullptr, 16, &wideIo) == nullptr && *__error_nid_postfix() == 22); + Require(ferror_nid_postfix(&wideIo)); + clearerr_nid_postfix(&wideIo); + Require(fgetws_nid_postfix(wideBuf, 16, nullptr) == nullptr && *__error_nid_postfix() == 22); + wideBuf[0] = u'X'; + wideBuf[1] = u'Y'; + std::rewind(wideIo.GetHandle()); + Require(fgetws_nid_postfix(wideBuf, 1, &wideIo) == wideBuf && wideBuf[0] == 0 && wideBuf[1] == u'Y'); + Require(fgetc_nid_postfix(&wideIo) == 'H'); + wideIo.Close(); + + FileStream roundTrip(std::tmpfile()); + Require(putwc_nid_postfix(u'é', &roundTrip) == u'é'); + Require(putwc_nid_postfix(u'\u20ac', &roundTrip) == u'\u20ac'); + Require(putwc_nid_postfix(u'\n', &roundTrip) == u'\n'); + std::rewind(roundTrip.GetHandle()); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'é')); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'\u20ac')); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'\n')); + Require(getwc_nid_postfix(&roundTrip) == -1); + std::rewind(roundTrip.GetHandle()); + char16_t utfBuf[16]{}; + Require(fgetws_nid_postfix(utfBuf, 16, &roundTrip) == utfBuf); + Require(utfBuf[0] == u'é' && utfBuf[1] == u'\u20ac' && utfBuf[2] == u'\n' && utfBuf[3] == 0); + roundTrip.Close(); + + FileStream midErr(std::tmpfile()); + Require(fputc_nid_postfix('A', &midErr) == 'A'); + Require(fputc_nid_postfix(0xff, &midErr) == 0xff); + std::rewind(midErr.GetHandle()); + char16_t midBuf[16]{}; + Require(fgetws_nid_postfix(midBuf, 16, &midErr) == nullptr); + Require(ferror_nid_postfix(&midErr)); + midErr.Close(); + FileStream formatted(std::tmpfile()); const char expected[] = "guest 4294967297 1.25 1 2 3 4 5 6 7 8\n"; Require(fprintf_nid_postfix(&formatted, "%s %ld %.2f %d %d %d %d %d %d %d %d\n", diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp new file mode 100644 index 0000000000..7167b5003f --- /dev/null +++ b/core/libs/tests/GuestSysarch.cpp @@ -0,0 +1,55 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sysarch(int number, void* args); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template static std::string Thrown(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + +int main() { + Require(Thrown([] { sysarch(129, nullptr); }) == "sysarch: operation 129 not implemented"); + Require(Thrown([] { sysarch(7, nullptr); }) == "sysarch: operation 7 not implemented"); +#if defined(__linux__) && defined(__x86_64__) + static std::uint64_t slots[2] = {0x1234567890abcdefull, 0}; + std::uint64_t base = reinterpret_cast(slots); + Require(sysarch(131, &base) == 0); + std::uint64_t read = 0; + Require(sysarch(130, &read) == 0 && read == base); + std::uint64_t through = 0; + asm volatile("movq %%gs:0, %0" : "=r"(through)); + Require(through == slots[0]); + std::uint64_t fs = 0; + Require(sysarch(128, &fs) == 0 && fs != 0); + std::uint64_t self = 0; + asm volatile("movq %%fs:0, %0" : "=r"(self)); + Require(self == fs); + std::uint64_t outside = 0x0000800000000000ull; + *__error_nid_postfix() = 0; + Require(sysarch(131, &outside) == -1 && *__error_nid_postfix() == 22); + Require(sysarch(130, &read) == 0 && read == base); + *__error_nid_postfix() = 0; + Require(sysarch(131, nullptr) == -1 && *__error_nid_postfix() == 14); + Require(sysarch(130, nullptr) == -1 && *__error_nid_postfix() == 14); + std::uint64_t zero = 0; + Require(sysarch(131, &zero) == 0 && sysarch(130, &read) == 0 && read == 0); +#else + std::uint64_t base = 0; + Require(Thrown([&base] { sysarch(130, &base); }) == "sysarch: segment bases on this host not implemented"); +#endif + return 0; +} diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4c7f86455c..8581fefef2 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -1,10 +1,16 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceSysmodule/ModuleTable.hpp" #include #include +#include #include #include #include +#ifdef _WIN32 +#include +#include +#endif #ifdef __linux__ #include #include @@ -103,9 +109,95 @@ void CheckUnwindInfo() { } #endif +std::uint16_t UnknownModuleId() { + for (std::uint32_t id = 0xffff; id != 0; --id) { + if (kModuleTable.find(id) == kModuleTable.end()) return static_cast(id); + } + std::abort(); +} + +void CheckUnknownIds() { + const std::uint16_t id = UnknownModuleId(); + const std::uint32_t internalId = id | kInternalFlag; + Require(kModuleTable.find(internalId) == kModuleTable.end()); + Require(sceSysmoduleIsLoaded(id) == 0); + Require(sceSysmoduleLoadModule(id) == 0); + Require(sceSysmoduleIsLoaded(id) == 0); + Require(sceSysmoduleUnloadModule(id) == 0); + Require(sceSysmoduleUnloadModule(id) == 0); + Require(sceSysmoduleIsLoaded(id) == 0); + Require(ThrowsRuntimeError([internalId] { sceSysmoduleLoadModuleInternal(internalId); })); + Require(ThrowsRuntimeError([internalId] { sceSysmoduleUnloadModuleInternal(internalId); })); + int result = 0x5a5a5a5a; + Require(ThrowsRuntimeError([internalId, &result] { sceSysmoduleLoadModuleInternalWithArg(internalId, 0, nullptr, 0, &result); })); + Require(result == 0x5a5a5a5a); +} + +#ifdef _WIN32 +void RequireUnwindMatchesRegion(const void* pointer) { + const auto address = reinterpret_cast(pointer); + MEMORY_BASIC_INFORMATION expected{}; + Require(VirtualQuery(pointer, &expected, sizeof(expected)) != 0); + char mapped[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), expected.BaseAddress, mapped, sizeof(mapped) - 1); + mapped[length] = '\0'; + for (int repeat = 0; repeat < 3; ++repeat) { + ModuleInfoForUnwind info{}; + Require(sceSysmoduleGetModuleInfoForUnwind(address, 0, &info) == 0); + Require(info.st_size == sizeof(ModuleInfoForUnwind)); + Require(info.seg0_addr == reinterpret_cast(expected.BaseAddress) && info.seg0_size == expected.RegionSize); + Require(std::strncmp(info.name, mapped, sizeof(info.name) - 1) == 0); + Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_addr == 0 && info.eh_frame_size == 0); + } +} + +void CheckUnwindInfo() { + RequireUnwindMatchesRegion(reinterpret_cast(&RequireUnwindMatchesRegion)); + RequireUnwindMatchesRegion(reinterpret_cast(&sceSysmoduleGetModuleInfoForUnwind)); + RequireUnwindMatchesRegion(reinterpret_cast(&GetModuleHandleW)); + const auto* host = reinterpret_cast(&GetModuleHandleW); + MEMORY_BASIC_INFORMATION hostRegion{}; + Require(VirtualQuery(host, &hostRegion, sizeof(hostRegion)) != 0); + for (const std::size_t step : {std::size_t{0x1000}, std::size_t{0x3000}, std::size_t{0x10000}}) { + if (hostRegion.RegionSize > step) RequireUnwindMatchesRegion(host + step); + } + RequireUnwindMatchesRegion(host + 0x2000); + RequireUnwindMatchesRegion(host); + const auto* anchor = reinterpret_cast(&RtlPcToFileHeader); + MEMORY_BASIC_INFORMATION anchorRegion{}; + Require(VirtualQuery(anchor, &anchorRegion, sizeof(anchorRegion)) != 0); + if (anchorRegion.RegionSize > 0x4000) { + RequireUnwindMatchesRegion(anchor + 0x3000); + RequireUnwindMatchesRegion(anchor); + RequireUnwindMatchesRegion(anchor + 0x1000); + RequireUnwindMatchesRegion(anchor + 0x2000); + } + static int data = 1; + static const char text[] = "unwind region"; + RequireUnwindMatchesRegion(&data); + RequireUnwindMatchesRegion(text); + RequireUnwindMatchesRegion(reinterpret_cast(&RequireUnwindMatchesRegion)); + int local = 0; + RequireUnwindMatchesRegion(&local); + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t page = system.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, 3 * page, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + Require(memory != nullptr); + RequireUnwindMatchesRegion(memory); + DWORD previous = 0; + Require(VirtualProtect(memory + 2 * page, page, PAGE_READONLY, &previous) != 0); + RequireUnwindMatchesRegion(memory); + RequireUnwindMatchesRegion(memory + 2 * page); + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0); + RequireUnwindMatchesRegion(memory); +} +#endif + } int main() { + CheckUnknownIds(); Require(sceSysmoduleIsLoaded(0) == kInvalidValue); Require(sceSysmoduleLoadModule(0) == kInvalidValue); Require(sceSysmoduleUnloadModule(0) == kInvalidValue); @@ -178,7 +270,7 @@ int main() { Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); -#ifdef __linux__ +#if defined(__linux__) || defined(_WIN32) CheckUnwindInfo(); #endif diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 330424b904..5c3f0922f4 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -67,6 +67,26 @@ static void CheckProcessorCountSysctl() { } Require(threw); } +static void CheckPageSizeSysctl() { + const int mib[] = {6, 7}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(int)); + int value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == 0x4000 && length == sizeof(int)); + value = 0; + length = 16; + Require(sysctlbyname_nid_postfix("hw.pagesize", &value, &length, nullptr, 0) == 0 && value == 0x4000 && length == sizeof(int)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("hw.pagesize", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("hw.pagesize", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} static void CheckPhysicalMemorySysctl(int number, const char* name) { const int mib[] = {6, number}; const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; @@ -178,7 +198,7 @@ static void CheckPathnameSysctl() { Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } static void CheckLeafWithTrailingComponent() { - const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {1, 37}}; + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -311,6 +331,7 @@ static void CheckNameToMib() { const Expected expected[] = { {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, + {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, @@ -396,6 +417,7 @@ static void CheckNameToMib() { } int main() { CheckProcessorCountSysctl(); + CheckPageSizeSysctl(); CheckPhysicalMemorySysctl(5, "hw.physmem"); CheckPhysicalMemorySysctl(12, "hw.realmem"); #if defined(__x86_64__) || defined(__i386__) diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 5242a51ef2..8fa6982485 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -88,10 +88,20 @@ void AppendMarker(const std::string& line) { Require(marker != nullptr); std::ofstream(marker, std::ios::app) << line << '\n'; } -void MainShutdown() { AppendMarker("shutdown"); } +void MainShutdown() { + AppendMarker("shutdown"); + std::thread(AwaitExit).detach(); + awaiting.wait(false); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} void SecondaryShutdown() { AppendMarker("secondary shutdown"); } void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } +void GuestExitShutdown() { AppendMarker("shutdown"); } +void APS5_VABI CheckShutdownStillRunning(void*) { + if (LibcShutdownToken_nid_postfix().stop_requested()) std::_Exit(8); + AppendMarker("guest finalizer"); +} int RunChild(const std::filesystem::path& self, const char* mode) { #ifdef _WIN32 std::wstring commandLine = L"\"" + self.wstring() + L"\" " + std::wstring(mode, mode + std::strlen(mode)); @@ -130,10 +140,27 @@ int DriveNestedExit() { std::error_code ignored; std::filesystem::remove(marker, ignored); Require(code == 7); - Require(text == "remaining handler\n"); + Require(text == "guest finalizer\nremaining handler\nshutdown\n"); std::puts("Guest nested exit completed"); return 0; } +int DriveExitOrder() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-order-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + const int code = RunChild(self, "--exit-order"); + std::string text; + { + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + Require(code == 0); + Require(text == "guest finalizer\nshutdown\n"); + std::puts("Guest finalizers ran before shutdown"); + return 0; +} int DriveRelaunch() { const auto self = OwnExecutable(); const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); @@ -159,6 +186,10 @@ int DriveRelaunch() { } int main(int argc, char** argv) { const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--requested-exit-driver") == 0) { + Require(RunChild(OwnExecutable(), "--requested-exit") == 0); + return 0; + } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--immediate") == 0) { LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); Require(std::atexit(UnexpectedCleanup) == 0); @@ -183,11 +214,22 @@ int main(int argc, char** argv) { return DriveNestedExit(); } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit") == 0) { + LibcRegisterShutdown_nid_postfix(GuestExitShutdown); Require(__cxa_atexit_nid_postfix(RemainingHandler, nullptr, nullptr) == 0); + Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); Require(__cxa_atexit_nid_postfix(ExitingHandler, nullptr, nullptr) == 0); exit_nid_postfix(0); return 2; } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-order-driver") == 0) { + return DriveExitOrder(); + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-order") == 0) { + LibcRegisterShutdown_nid_postfix(GuestExitShutdown); + Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); + exit_nid_postfix(0); + return 2; + } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch-driver") == 0) { return DriveRelaunch(); } @@ -217,6 +259,11 @@ int main(int argc, char** argv) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(__cxa_atexit_nid_postfix(UnexpectedFinalizer, nullptr, nullptr) == 0); Require(std::atexit(ExitHandlerRan) == 0); + if (std::strcmp(argv[firstArgument], "--requested-exit") == 0) { + LibcRequestExit_nid_postfix(0); + LibcRequestExit_nid_postfix(7); + LibcAwaitExit_nid_postfix(); + } sceSystemServiceLoadExec("exit", nullptr); return 2; } diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index ee6e1d7e52..426560081b 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -14,6 +14,7 @@ int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(std::int32_t, void*, std:: int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(std::int32_t); int APS5_VABI sceSystemGestureGetTouchEvents(std::int32_t, const void*, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(std::int32_t, std::uint32_t, void*); +int APS5_VABI sceSystemGestureGetTouchEventByIndex(std::int32_t, const void*, std::uint32_t, void*); int APS5_VABI sceSystemGestureAppendTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureCreateTouchRecognizer(std::int32_t, void*, std::int32_t, const void*, const void*); int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(std::int32_t, const void*, void*); @@ -108,6 +109,9 @@ int main() { Require(sceSystemGestureGetTouchEvents(handle + 1, &recognizer, buffer.data(), 4, &count) == invalidHandle); Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, buffer.data()) == indexOutOfArray); Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle + 1, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByIndex(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); + Require(sceSystemGestureGetTouchEventByIndex(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByIndex(handle, nullptr, 0, buffer.data()) == invalidArgument); Require(buffer == original); Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == 0); diff --git a/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp new file mode 100644 index 0000000000..ec83ac1c10 --- /dev/null +++ b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp @@ -0,0 +1,74 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); +int APS5_VABI dup2_nid_postfix(int, int); +} + +namespace { + +constexpr const char* Marker = "APS5_LOAD_EXEC_STANDARD_ERROR"; +constexpr const char* AfterLoadExec = "--after-load-exec"; + +void Require(bool value) { + if (!value) std::abort(); +} + +std::string Drain(int descriptor) { + std::string text; + char buffer[4096]; + for (ssize_t count; (count = ::read(descriptor, buffer, sizeof(buffer))) > 0;) text.append(buffer, static_cast(count)); + ::close(descriptor); + return text; +} + +void Run(const char* mode, const char* const* arguments) { + int host[2] = {-1, -1}; + int guest[2] = {-1, -1}; + Require(::pipe(host) == 0); + Require(::pipe(guest) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + ::close(guest[0]); + if (::dup2(host[1], 2) != 2) ::_exit(70); + ::close(host[1]); + if (dup2_nid_postfix(guest[1], 2) != 2) ::_exit(71); + ::close(guest[1]); + if (::setenv(Marker, mode, 1) != 0) ::_exit(72); + sceSystemServiceLoadExec("/app0/eboot.bin", arguments); + ::_exit(73); + } + ::close(host[1]); + ::close(guest[1]); + const std::string hostText = Drain(host[0]); + const std::string guestText = Drain(guest[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(hostText == std::string("started ") + mode + "\n"); + Require(guestText.empty()); +} + +} + +int main(int argc, char** argv) { + if (const char* mode = std::getenv(Marker); mode != nullptr) { + if (std::strcmp(mode, "list") == 0) Require(argc == 1 && std::strcmp(argv[0], AfterLoadExec) == 0); + else Require(std::strcmp(mode, "none") == 0 && argc == 1); + std::fprintf(stderr, "started %s\n", mode); + return 0; + } + const char* const arguments[] = {AfterLoadExec, nullptr}; + Run("list", arguments); + Run("none", nullptr); + std::puts("LoadExec standard error tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestSystemServiceTimeZone.cpp b/core/libs/tests/GuestSystemServiceTimeZone.cpp new file mode 100644 index 0000000000..7f61bcf1fe --- /dev/null +++ b/core/libs/tests/GuestSystemServiceTimeZone.cpp @@ -0,0 +1,57 @@ +#include "SceTypes.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include +#include + +extern "C" int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value); + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void SetTimeZone(const char* zone) { +#ifdef _WIN32 + _putenv_s("TZ", zone); + _tzset(); +#else + setenv("TZ", zone, 1); + tzset(); +#endif +} + +int ParamInt(int paramId) { + int value = -1; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_OK); + return value; +} + +bool HostSummerTime() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#ifdef _WIN32 + Require(localtime_s(&local, &now) == 0); +#else + Require(localtime_r(&now, &local) != nullptr); +#endif + return local.tm_isdst > 0; +} + +void RequireZone(const char* zone, int minutes, int summer) { + SetTimeZone(zone); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_TIME_ZONE) == minutes); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_SUMMERTIME) == summer); +} + +} + +int main() { + RequireZone("IST-5:30", 330, 0); + RequireZone("BRT+3", -180, 0); + RequireZone("UTC0", 0, 0); + SetTimeZone("CET-1CEST"); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_TIME_ZONE) == 60); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_SUMMERTIME) == (HostSummerTime() ? 1 : 0)); +#ifndef _WIN32 + RequireZone("CET-1CEST,0/0,J365/25", 60, 1); +#endif +} diff --git a/core/libs/tests/GuestThermalSensors.cpp b/core/libs/tests/GuestThermalSensors.cpp index a62992aba8..3c051911a6 100644 --- a/core/libs/tests/GuestThermalSensors.cpp +++ b/core/libs/tests/GuestThermalSensors.cpp @@ -73,6 +73,9 @@ int main() { int probe = -12345; const int result = sceKernelGetSocSensorTemperature(0, &probe); Require(result == 0 ? (probe >= 0 && probe <= 130) : (result == SCE_KERNEL_ERROR_EINVAL && probe == -12345)); + int cpuProbe = -12345; + const int cpuResult = sceKernelGetCpuTemperature(&cpuProbe); + Require(cpuResult == 0 ? (cpuProbe >= 0 && cpuProbe <= 130) : (cpuResult == SCE_KERNEL_ERROR_EINVAL && cpuProbe == -12345)); Require(sceKernelGetSocPowerConsumption(nullptr) == SCE_KERNEL_ERROR_EFAULT); std::uint32_t consumption[32]; diff --git a/core/libs/tests/GuestTruncate.cpp b/core/libs/tests/GuestTruncate.cpp index 0f8d4b1e50..1e037fd9d8 100644 --- a/core/libs/tests/GuestTruncate.cpp +++ b/core/libs/tests/GuestTruncate.cpp @@ -5,12 +5,15 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include +#include +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); #else #include #endif @@ -18,12 +21,17 @@ extern "C" { int APS5_VABI truncate_nid_postfix(const char*, std::int64_t); int APS5_VABI sceKernelTruncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI sceKernelFtruncate(int, std::int64_t); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } constexpr int GuestEnoent = 2; +constexpr int GuestEbadf = 9; constexpr int GuestEacces = 13; constexpr int GuestEfault = 14; constexpr int GuestEisdir = 21; @@ -40,6 +48,31 @@ static std::string Contents(const std::filesystem::path& file) { return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } +#ifdef _WIN32 +static unsigned parameterReports = 0; + +static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { + ++parameterReports; +} + +static void CheckDescriptorFailures(const char* path) { + const int closed = sceKernelOpen(path, 2, 0); + Require(closed >= 0 && sceKernelClose(closed) == 0); + const auto previous = _set_thread_local_invalid_parameter_handler(CountParameterReport); + for (const int descriptor : {-2, -1, closed, std::numeric_limits::max()}) { + for (const std::int64_t length : {std::int64_t{0}, std::int64_t{8}}) { + RequireFailure(ftruncate_nid_postfix(descriptor, length), GuestEbadf); + Require(sceKernelFtruncate(descriptor, length) == SceKernelErrorBase + GuestEbadf); + } + } + Require(_set_thread_local_invalid_parameter_handler(previous) == CountParameterReport); + if (parameterReports != 0) std::fprintf(stderr, "ftruncate invoked the CRT parameter handler %u times\n", parameterReports); + Require(parameterReports == 0); + RequireFailure(ftruncate_nid_postfix(closed, 0), GuestEbadf); + Require(sceKernelFtruncate(-1, 0) == SceKernelErrorBase + GuestEbadf); +} +#endif + int main() { const auto name = "anyps5-truncate-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); const auto directory = std::filesystem::path(name); @@ -47,6 +80,10 @@ int main() { const auto file = directory / "sized.txt"; { std::ofstream output(file, std::ios::binary); output << "0123456789abcdef"; } const auto path = file.string(); +#ifdef _WIN32 + CheckDescriptorFailures(path.c_str()); + Require(Contents(file) == "0123456789abcdef"); +#endif Require(truncate_nid_postfix(path.c_str(), 6) == 0); Require(std::filesystem::file_size(file) == 6); diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index 48ec61d508..56abb976c2 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -123,19 +123,20 @@ static void TimesOutWhileTheValueMatches() { static void RejectsAnInvalidTimeout() { std::uint32_t word = 1; KernelTimespec negative{-1, 0}; - Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negative)), &negative), GUEST_EINVAL)); KernelTimespec tooManyNanos{0, 1000000000}; - Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(tooManyNanos)), &tooManyNanos), GUEST_EINVAL)); for (const int operation : {UMTX_OP_WAIT, UMTX_OP_WAIT_UINT}) { - bool threw = false; - try { Umtx(&word, operation, 1, nullptr, &tooManyNanos); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); - threw = false; - try { Umtx(&word, operation, 1, nullptr, &negative); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(FailsWith(Umtx(&word, operation, 1, SizeOf(sizeof(negative)), &negative), GUEST_EINVAL)); + Require(FailsWith(Umtx(&word, operation, 1, SizeOf(sizeof(tooManyNanos)), &tooManyNanos), GUEST_EINVAL)); } + struct { + std::uint32_t usecs = 20000; + std::uint32_t zero32 = 0; + std::int64_t zero64 = 0; + } zeroNeighbours; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &zeroNeighbours), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); UmtxTime negativeNanos{{0, -1}, 0, CLOCK_MONOTONIC_ID}; Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negativeNanos)), &negativeNanos), GUEST_EINVAL)); } @@ -146,7 +147,7 @@ static void WakeReleasesAWaiterWithoutAValueChange() { int result = -2; std::thread waiter([&] { KernelTimespec failsafe = FailsafeTimeout(); - result = Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &failsafe); + result = Umtx(&value, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); while (finished.load() == 0) { @@ -171,14 +172,14 @@ static void WakeHonoursTheCountAndTheAddress() { waiters.emplace_back([&, i] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); } std::thread otherWaiter([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++otherFinished; }); AwaitCount(started, 5); @@ -227,20 +228,20 @@ static void NwakeWakesEveryWaiterOnEachListedAddress() { waiters.emplace_back([&, i] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); } waiters.emplace_back([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[3] = Umtx(&second, UMTX_OP_WAIT, 1, nullptr, &failsafe); + results[3] = Umtx(&second, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); std::thread unlistedWaiter([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++unlistedFinished; }); AwaitCount(started, 5); diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 4604a40034..72f76ec270 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -120,9 +120,12 @@ int APS5_VABI sceVdecswCreateDecoder(const DecoderConfigInfo*, const DecoderMemo int APS5_VABI sceVdecswDeleteDecoder(std::uint64_t); int APS5_VABI sceVdecswSetDecodeInput(std::uint64_t, const InputData*); int APS5_VABI sceVdecswTrySyncDecodeInput(std::uint64_t, InputResult*); +int APS5_VABI sceVdecswSyncDecodeInput(std::uint64_t, InputResult*); int APS5_VABI sceVdecswSetDecodeOutput(std::uint64_t, const FrameBuffer*); int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t, OutputInfo*); +int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, void*); int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t); int APS5_VABI sceVdecswResetDecoder(std::uint64_t); } @@ -233,12 +236,37 @@ void testWithoutOutput() { check(sceVdecswTrySyncDecodeInput(session.decoder, &result) == InputQueueEmpty, "a reset decoder kept its inputs"); } +void testSyncWrappers() { + Session session; + InputResult idle{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &idle) == InputQueueEmpty, "sync input on an idle decoder did not report an empty queue"); + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "decode input was rejected"); + InputResult result{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &result) == 0 && result.decodedAu == units[0].data(), "sync input did not consume the access unit"); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "output buffer was rejected"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.isLastFrame, "sync output did not return the finalized picture"); + bool hevcThrows = false; + try { + sceVdecswGetHevcPictureInfo(&output, &result); + } catch (const std::exception&) { + hevcThrows = true; + } + check(hevcThrows, "GetHevcPictureInfo must still throw"); +} + } int main() { try { testDecode(); testWithoutOutput(); + testSyncWrappers(); std::puts("Vdecsw tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/GuestVideoOutColorSpace.cpp b/core/libs/tests/GuestVideoOutColorSpace.cpp new file mode 100644 index 0000000000..1c081f65f9 --- /dev/null +++ b/core/libs/tests/GuestVideoOutColorSpace.cpp @@ -0,0 +1,93 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param); +int APS5_VABI sceVideoOutClose(int handle); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option); +} + +static constexpr int SYSTEM_USER = 255; +static constexpr int MAIN_BUS = 0; + +static void Require(bool value) { + if (!value) std::abort(); +} + +static void ExpectFailure(auto&& action) { + bool failed = false; + try { + action(); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); +} + +int main() { + VideoOutTestEnvironment environment; + VideoOutColorSpaceConversionSettings settings{}; + std::memset(&settings, 0x5A, sizeof(settings)); + + ExpectFailure([] { sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(nullptr); }); + Require(sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(&settings) == 0); + for (std::size_t index = 0; index < 8; ++index) { + Require(settings.internalData[index] == 0); + } + + ExpectFailure([] { sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(nullptr, 1.0f, 0); }); + Require(sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(&settings, 1.0f, 0) == 0); + + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(0, nullptr); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(-1, &settings); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(0, &settings); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(1, &settings); }); + + ExpectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&settings, sizeof(settings) - 1); }); + Require(sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&settings, sizeof(settings)) == 0); + + ExpectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&settings, 1.0f, 0, sizeof(settings) - 1); }); + Require(sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&settings, 1.0f, 0, sizeof(settings)) == 0); + + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(1, &settings, sizeof(settings) - 1); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(1, &settings, sizeof(settings)); }); + + ExpectFailure([] { sceVideoOutSetGlobalBlendSpace(-1, 0, 0); }); + ExpectFailure([] { sceVideoOutSetGlobalBlendSpace(0, 0, 0); }); + ExpectFailure([] { sceVideoOutSetGlobalBlendSpace(1, 0, 0); }); + + int handle = 0; + try { + handle = sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr); + } catch (const std::runtime_error& error) { + if (std::getenv("ANYPS5_REQUIRE_DISPLAY") != nullptr) throw; + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + Require(handle > 0); + + Require(sceVideoOutAdjustColorSpaceConversion(handle, &settings) == 0); + Require(sceVideoOutAdjustColorSpaceConversion_(handle, &settings, sizeof(settings)) == 0); + Require(sceVideoOutSetGlobalBlendSpace(handle, 0, 0) == 0); + + Require(sceVideoOutClose(handle) == 0); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(handle, &settings); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(handle, &settings, sizeof(settings)); }); + ExpectFailure([&] { sceVideoOutSetGlobalBlendSpace(handle, 0, 0); }); + + LibcRunShutdown_nid_postfix(); + return 0; +} diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 9efbaa0456..f8d3be0ffc 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -509,7 +509,7 @@ void testTenBit() { } } -void testDecode(bool lengthPrefixed) { +void testDecode(bool lengthPrefixed, bool separateParameters = false) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 0, 1, 100, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); @@ -534,8 +534,29 @@ void testDecode(bool lengthPrefixed) { ++pictures; return true; }; - const auto units = accessUnits(lengthPrefixed); + auto units = accessUnits(lengthPrefixed); check(units.size() == PictureHashes.size(), "unexpected access unit count"); + if (separateParameters) { + auto& first = units.front(); + std::size_t slice = 0; + for (std::size_t index = 0; index + 3 < first.size(); ++index) { + if (first[index] != 0 || first[index + 1] != 0 || first[index + 2] != 1) continue; + const auto type = first[index + 3] & 0x1fu; + if (type >= 1 && type <= 5) { + slice = index > 0 && first[index - 1] == 0 ? index - 1 : index; + break; + } + index += 2; + } + check(slice != 0, "the first access unit has no slice after its parameter sets"); + const std::vector parameters(first.begin(), first.begin() + static_cast(slice)); + first.erase(first.begin(), first.begin() + static_cast(slice)); + const InputData input{sizeof(InputData), parameters.data(), parameters.size(), 999, 0, 0x9f}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "a unit of parameter sets alone was not accepted"); + check(!output.isValid && !frame.isAccepted, "a unit of parameter sets alone produced a picture"); + } for (std::size_t unit = 0; unit < units.size(); ++unit) { const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; @@ -625,6 +646,42 @@ std::uint64_t frameBufferBytes() { return memory.maxFrameBufferSize; } +void testDecodeAfterFlush() { + std::vector buffer(frameBufferBytes()); + const auto handle = createDecoder(1, 100); + { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0 && !output.isValid && !frame.isAccepted, "flushing a decoder that decoded nothing gave a picture"); + } + const auto units = accessUnits(false); + for (const std::uint64_t base : {1000u, 5000u}) { + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(pictures < PictureHashes.size() && owned(buffer, base + DisplayOrderUnits[pictures]), "a picture decoded after a flush is out of display order"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " decoded after a flush differs from the reference"); + ++pictures; + return true; + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), base + unit, unit, base + unit}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode after a flush failed"); + take(output); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output)) break; + } + check(pictures == PictureHashes.size(), "missing pictures of a stream decoded after a flush"); + } + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + void testPictureOwnership() { std::vector shared(frameBufferBytes()), other(frameBufferBytes()); { @@ -727,6 +784,7 @@ int main() { testFailures(); testDecode(false); testDecode(true); + testDecode(false, true); const auto hevc = hevcUnits(HevcStream.data(), HevcStream.size()); testClip(CodecHevc, 1, hevc, HevcHashes); std::vector> hevcLengthPrefixed; @@ -752,6 +810,7 @@ int main() { testClip(CodecVp9, 0, ivfFrames(), Vp9Hashes); testVp9PictureMetadata(); testTenBit(); + testDecodeAfterFlush(); testPictureOwnership(); testConcurrentOwnership(); std::puts("Videodec2 tests passed"); diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 8d1d0eeeb4..9b244b42d1 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t, bool*); int APS5_VABI sceVoiceGetPortInfo(std::uint32_t, VoicePortInfo*); int APS5_VABI sceVoiceReadFromOPort(std::uint32_t, void*, std::uint32_t*); int APS5_VABI sceVoiceResetPort(std::uint32_t); +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); } namespace { @@ -44,10 +45,19 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { int main() { RequireThrows([] { sceVoiceResetPort(0); }); + RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); VoiceInitParam init{}; VoiceStartParam start{}; Require(sceVoiceInit(&init, 0) == 0); Require(sceVoiceStart(&start) == 0); + RequireThrows([] { sceVoiceGetResourceInfo(nullptr); }); + VoiceResourceInfo resourceInfo{}; + Require(sceVoiceGetResourceInfo(&resourceInfo) == 0); + Require(resourceInfo.max_voice_input_ports == 7); + Require(resourceInfo.max_voice_output_ports == 7); + Require(resourceInfo.max_device_input_ports == 1); + Require(resourceInfo.max_device_output_ports == 1); + Require(resourceInfo.max_ports == 64); const auto input = CreatePort(2, 0.5f); const auto output = CreatePort(3, 0.25f); Require(sceVoiceConnectIPortToOPort(input, output) == 0); @@ -76,4 +86,5 @@ int main() { Require(sceVoiceResetPort(output) == 0); Require(sceVoiceEnd_nid_postfix() == 0); RequireThrows([&] { sceVoiceResetPort(output); }); + RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); } diff --git a/core/libs/tests/GuestVoicePortAttr.cpp b/core/libs/tests/GuestVoicePortAttr.cpp new file mode 100644 index 0000000000..3264ec11c4 --- /dev/null +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -0,0 +1,59 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceInit(VoiceInitParam*, int32_t); +int APS5_VABI sceVoiceEnd_nid_postfix(void); +int APS5_VABI sceVoiceCreatePort(uint32_t*, const VoicePortParam*); +int APS5_VABI sceVoiceDeletePort(uint32_t); +int APS5_VABI sceVoiceGetPortAttr(uint32_t, int32_t, void*, int32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool Throws(Call call, std::string_view message) { + try { + call(); + } catch (const std::logic_error& error) { + return std::string_view(error.what()).find(message) != std::string_view::npos; + } + return false; +} + +int main() { + constexpr std::int32_t voiceActivity = 1001; + + Require(Throws([] { sceVoiceGetPortAttr(0, voiceActivity, nullptr, 1); }, "library not initialized")); + + VoiceInitParam init{}; + Require(sceVoiceInit(&init, 0) == 0); + + VoicePortParam param{}; + param.port_type = 2; + param.volume = 1.0f; + param.voice.bitrate = 16000; + std::uint32_t port = 0; + Require(sceVoiceCreatePort(&port, ¶m) == 0); + + std::array buffer{0xa5, 0xa5, 0xa5}; + Require(sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1) == 0); + Require(buffer[0] == 0xa5 && buffer[1] == 0 && buffer[2] == 0xa5); + + Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, 1, &buffer[1], 1); }, "attribute 1 not implemented")); + + Require(Throws([&buffer] { sceVoiceGetPortAttr(0x1234, voiceActivity, &buffer[1], 1); }, "unknown port")); + + Require(sceVoiceDeletePort(port) == 0); + Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1); }, "unknown port")); + + Require(sceVoiceEnd_nid_postfix() == 0); + return 0; +} diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 44aa996344..cfe4c4f3bb 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -6,6 +6,14 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); int APS5_VABI sceVoiceQoSEnd(); +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(std::int32_t*, std::int32_t, std::int32_t, std::int32_t); +int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(std::int32_t); +int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(std::int32_t*); +int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(std::int32_t); +int APS5_VABI sceVoiceQoSConnect(std::int32_t*, std::int32_t, std::int32_t); +int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); +int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); +int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -28,11 +36,55 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + + std::int32_t local = -1; + std::int32_t remote = -1; + std::int32_t connection = -1; + Require(sceVoiceQoSCreateLocalEndpoint(nullptr, 1, 0, 0) == argumentInvalid); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == 0 && local > 0); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote > 0 && remote != local); + Require(sceVoiceQoSConnect(&connection, remote, local) == argumentInvalid); + Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection > 0); + std::int32_t duplicate = -1; + Require(sceVoiceQoSConnect(&duplicate, local, remote) == 0 && duplicate > 0 && duplicate != connection); + Require(sceVoiceQoSDisconnect(duplicate) == 0); + + std::int32_t otherLocal = -1; + std::int32_t otherConnection = -1; + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 1, 0, 0) == 0); + Require(sceVoiceQoSConnect(&otherConnection, otherLocal, remote) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == argumentInvalid); + Require(sceVoiceQoSDisconnect(otherConnection) == 0); + + const std::uint32_t level = 0x1234; + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); + + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, 0) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + + Require(sceVoiceQoSDisconnect(connection) == 0); + Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == 0); + + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); - Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); - Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); - Require(sceVoiceQoSEnd() == 0); - Require(sceVoiceQoSEnd() == notInitialized); } diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index a3afc479f3..e747df363a 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -15,6 +15,8 @@ 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); @@ -30,10 +32,13 @@ int main() { Require(sceVrSetupDialogTerminate() == kErrNotInitialized); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrSetupDialogClose() == kErrNotInitialized); Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); Require(sceVrSetupDialogInitialize() == 0); Require(sceVrSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); @@ -49,4 +54,5 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index b20ec63097..9fc91930ac 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void); int APS5_VABI sceWebBrowserDialogTerminate(void); int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param); int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); @@ -64,5 +65,20 @@ int main() { Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpenForPredeterminedContent(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + + result = 0x5a5a5a5a; + 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/GuestWorkingDirectory.cpp b/core/libs/tests/GuestWorkingDirectory.cpp index df874768e8..de3ea75fc1 100644 --- a/core/libs/tests/GuestWorkingDirectory.cpp +++ b/core/libs/tests/GuestWorkingDirectory.cpp @@ -3,9 +3,11 @@ #include "prx/libc/include/GuestHeap.hpp" #include #include +#include #include #include #include +#include extern "C" { int APS5_VABI chdir_nid_postfix(const char*); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); @@ -41,6 +43,14 @@ int main() { Require(allocated && std::strcmp(allocated, path) == 0 && allocations == 1); ApplicationHeapFree_nid_no_patch(allocated); Require(releases == 1); + for (const auto size : {std::size_t{1}, std::size_t{2}, std::size_t{1024}, std::numeric_limits::max()}) { + const int before = allocations; + allocated = getcwd_nid_postfix(nullptr, size); + if (!allocated) std::fputs("getcwd(NULL, size) must ignore size when allocating its result\n", stderr); + Require(allocated && std::strcmp(allocated, path) == 0 && allocations == before + 1); + ApplicationHeapFree_nid_no_patch(allocated); + Require(releases == allocations); + } Require(chdir_nid_postfix("sample.txt") == -1 && *__error_nid_postfix() == 20); Require(chdir_nid_postfix("missing") == -1 && *__error_nid_postfix() == 2); Require(chdir_nid_postfix("..") == 0); diff --git a/core/libs/tests/HostLibstdcxxException.cpp b/core/libs/tests/HostLibstdcxxException.cpp new file mode 100644 index 0000000000..192682cbe8 --- /dev/null +++ b/core/libs/tests/HostLibstdcxxException.cpp @@ -0,0 +1,15 @@ +#include +#include +#include + +extern "C" const char* HostLibstdcxxExceptionFailure(const char* path); + +int main() { + const std::string path = "/proc/" + std::to_string(getpid()) + "/missing/file"; + if (const char* failure = HostLibstdcxxExceptionFailure(path.c_str())) { + std::fprintf(stderr, "%s\n", failure); + return 1; + } + std::puts("host libstdc++ exceptions caught"); + return 0; +} diff --git a/core/libs/tests/HostLibstdcxxExceptionModule.cpp b/core/libs/tests/HostLibstdcxxExceptionModule.cpp new file mode 100644 index 0000000000..aeb11b5a9d --- /dev/null +++ b/core/libs/tests/HostLibstdcxxExceptionModule.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include + +static bool CatchesConversion() { + try { + (void)std::stoi("x"); + } catch (const std::invalid_argument& error) { + return std::string(error.what()) == "stoi"; + } + return false; +} + +static bool CatchesStreamFailure(const char* path) { + try { + std::ofstream file; + file.exceptions(std::ios::failbit); + file.open(path); + } catch (const std::ios_base::failure&) { + return true; + } + return false; +} + +static bool CatchesStreamFailureAsException(const char* path) { + try { + std::ofstream file; + file.exceptions(std::ios::failbit); + file.open(path); + } catch (const std::exception&) { + return true; + } + return false; +} + +static bool RethrowsStoredStreamFailure(const char* path) { + std::exception_ptr failure; + try { + std::ofstream file; + file.exceptions(std::ios::failbit); + file.open(path); + } catch (...) { + failure = std::current_exception(); + } + if (!failure) return false; + try { + std::rethrow_exception(failure); + } catch (const std::ios_base::failure&) { + return true; + } + return false; +} + +extern "C" const char* HostLibstdcxxExceptionFailure(const char* path) { + if (!CatchesConversion()) return "std::stoi failure was not caught as std::invalid_argument"; + if (!CatchesStreamFailure(path)) return "std::ofstream::open failure was not caught as std::ios_base::failure"; + if (!CatchesStreamFailureAsException(path)) return "std::ofstream::open failure was not caught as std::exception"; + if (!RethrowsStoredStreamFailure(path)) return "a stored std::ofstream::open failure was not rethrown"; + return nullptr; +} diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 719a0852ff..8d9a2ffbd9 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -102,11 +102,12 @@ static void CheckFlexibleMemorySize(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; Require(!ParseText(path, prefix + "}").flexibleMemorySizeBytes, "Absent kernel changed the default budget"); Require(!ParseText(path, prefix + R"(,"kernel":{}})").flexibleMemorySizeBytes, "Absent flexible size changed the default budget"); + Require(!ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":0}})").flexibleMemorySizeBytes, "A zero flexible size changed the default budget"); for (const auto bytes : {std::uint64_t{16384}, std::uint64_t{838860800}, std::uint64_t{18446744073709535232ull}}) { const auto parsed = ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + std::to_string(bytes) + "}}"); Require(parsed.flexibleMemorySizeBytes == bytes, "Incorrect flexible memory size in bytes"); } - for (const auto* value : {"0", "-0", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { + for (const auto* value : {"-0", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { bool rejected = false; try { ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + value + "}}"); } catch (const std::runtime_error&) { rejected = true; } diff --git a/core/libs/tests/PreciseWait.cpp b/core/libs/tests/PreciseWait.cpp index 4089cb4abb..cf7d7d6979 100644 --- a/core/libs/tests/PreciseWait.cpp +++ b/core/libs/tests/PreciseWait.cpp @@ -1,14 +1,178 @@ #include "prx/libc/include/PreciseWait.hpp" +#include +#include #include +#include +#include #include +namespace { + +bool IsRunnerPreempted() { +#ifdef _WIN32 + char skip[8]{}; + const DWORD len = GetEnvironmentVariableA("APS5_SKIP_TIMING_TESTS", skip, sizeof(skip)); + if (len > 0 && skip[0] != '0') { + return true; + } +#else + if (const char* skip = std::getenv("APS5_SKIP_TIMING_TESTS"); skip != nullptr && skip[0] != '\0' && skip[0] != '0') { + return true; + } +#endif + const auto probeStart = std::chrono::steady_clock::now(); + auto prev = probeStart; + std::chrono::nanoseconds maxGap{0}; + while (prev - probeStart < std::chrono::milliseconds(2)) { + const auto cur = std::chrono::steady_clock::now(); + const auto gap = cur - prev; + if (gap > maxGap) { + maxGap = gap; + } + prev = cur; + } + return maxGap > std::chrono::microseconds(25); +} + +template +bool CheckMedianSleepBound( + const char* label, + std::chrono::nanoseconds minDuration, + std::chrono::nanoseconds maxMedian, + Fn&& sleepFn) { + std::array samples{}; + std::chrono::nanoseconds bestMedian{}; + for (int attempt = 0; attempt < 3; ++attempt) { + for (auto& sample : samples) { + const auto start = std::chrono::steady_clock::now(); + sleepFn(); + sample = std::chrono::steady_clock::now() - start; + if (sample < minDuration) { + std::cerr << label << " returned before its requested duration\n"; + return false; + } + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + if (attempt == 0 || median < bestMedian) { + bestMedian = median; + } + if (bestMedian < maxMedian) { + return true; + } + } + if (IsRunnerPreempted() || samples.back() - samples.front() > std::chrono::milliseconds(1)) { + std::clog << "Skipping strict median bound for " << label << " on busy runner (" + << std::chrono::duration_cast(bestMedian).count() << " ns)\n"; + return true; + } + std::cerr << label << " median overslept target bound: " + << std::chrono::duration_cast(bestMedian).count() << " ns\n"; + return false; +} + +} // namespace + int main() { - const auto start = std::chrono::steady_clock::now(); - PreciseSleepUs(50); - const auto elapsed = std::chrono::steady_clock::now() - start; - if (elapsed < std::chrono::microseconds(50)) { - std::cerr << "PreciseSleepUs returned before its requested duration\n"; + static_assert(PreciseHundredNanosToQpcTicks(500, 10000000ULL) == 500); + static_assert(PreciseHundredNanosToQpcTicks(500, 3579545ULL) == 179); + static_assert(PreciseHundredNanosToQpcTicks(500, 14318180ULL) == 716); + static_assert(PreciseHundredNanosToQpcTicks(500, 24000000ULL) == 1200); + static_assert(PrecisePauseThresholdTicks(3579545ULL) == 1); + static_assert(PrecisePauseThresholdTicks(10000000ULL) == 2); + static_assert(PrecisePauseThresholdTicks(24000000ULL) == 5); + + for (const std::uint64_t freq : {3579545ULL, 10000000ULL, 14318180ULL, 19200000ULL, 24000000ULL}) { + for (const unsigned long long hundredNanos : {1ULL, 5ULL, 100ULL, 300ULL, 500ULL, 10000ULL, 53330ULL}) { + const std::uint64_t ticks = PreciseHundredNanosToQpcTicks(hundredNanos, freq); + if (ticks * 10000000ULL < hundredNanos * freq) { + std::cerr << "PreciseHundredNanosToQpcTicks rounded below requested duration\n"; + return 1; + } + } + } + + PreciseSleepUs(0); + + for (const unsigned long long micros : {50ULL, 1000ULL}) { + const auto start = std::chrono::steady_clock::now(); + PreciseSleepUs(micros); + const auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed < std::chrono::microseconds(micros)) { + std::cerr << "PreciseSleepUs returned before its requested duration\n"; + return 1; + } + } + +#if !defined(_WIN32) + if (!CheckMedianSleepBound( + "PreciseSleepUs(50)", + std::chrono::microseconds(50), + std::chrono::microseconds(90), + [] { PreciseSleepUs(50); })) { return 1; } +#endif + + if (!CheckMedianSleepBound( + "PreciseSleepUntil(+50us)", + std::chrono::microseconds(50), + std::chrono::microseconds(90), + [] { PreciseSleepUntil(std::chrono::steady_clock::now() + std::chrono::microseconds(50)); })) { + return 1; + } + + if (!CheckMedianSleepBound( + "PreciseSleepUs(1500)", + std::chrono::microseconds(1500), + std::chrono::microseconds(1650), + [] { PreciseSleepUs(1500); })) { + return 1; + } + + const auto subMicroDeadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(500); + PreciseSleepUntil(subMicroDeadline); + if (std::chrono::steady_clock::now() < subMicroDeadline) { + std::cerr << "PreciseSleepUntil returned before sub-microsecond deadline\n"; + return 1; + } + + const auto millisecondDeadline = std::chrono::steady_clock::now() + std::chrono::microseconds(1000); + PreciseSleepUntil(millisecondDeadline); + if (std::chrono::steady_clock::now() < millisecondDeadline) { + std::cerr << "PreciseSleepUntil returned before millisecond deadline\n"; + return 1; + } + +#ifdef _WIN32 + for (const HANDLE badHandle : {static_cast(nullptr), GetCurrentProcess()}) { + const auto start = std::chrono::steady_clock::now(); + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + PreciseSleepFromCounterWithTimer(counter, 10000ULL, badHandle, true); + const auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed < std::chrono::microseconds(1000)) { + std::cerr << "PreciseSleepFromCounterWithTimer fallback returned early\n"; + return 1; + } + } +#endif + +#ifdef __linux__ + if (PreciseLinuxLowTimerSlack() && prctl(PR_GET_TIMERSLACK, 0UL, 0UL, 0UL, 0UL) != 1L) { + std::cerr << "PreciseLinuxLowTimerSlack did not configure 1ns timer slack\n"; + return 1; + } + for (const unsigned long long nanos : {80000ULL, 1000000ULL}) { + const auto start = std::chrono::steady_clock::now(); + PreciseSleepFromNanosWithSlack(PreciseMonotonicNanos(), nanos, false); + const auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed < std::chrono::nanoseconds(nanos)) { + std::cerr << "PreciseSleepFromNanosWithSlack conservative fallback returned early\n"; + return 1; + } + } +#endif + return 0; } diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 1c6f0f10e6..4f516231a5 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -20,10 +20,16 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(); +int APS5_VABI sceSaveDataCreateTransactionResource(std::uint32_t); +int APS5_VABI sceSaveDataDeleteTransactionResource(std::int32_t); int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2*); int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); } +namespace { +constexpr int kSaveDataNotInitialized = -2137063423; +} + static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "Save-data memory check failed at line %d\n", line); @@ -39,6 +45,8 @@ static std::vector Read(const std::filesystem::path& path) { } int main() { + Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); + Require(sceSaveDataDeleteTransactionResource(1) == kSaveDataNotInitialized); const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-metadata-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -54,6 +62,12 @@ int main() { Require(static_cast(file)); } Require(sceSaveDataInitialize3(nullptr) == 0); + const int tx0 = sceSaveDataCreateTransactionResource(0); + Require(tx0 > 0); + Require(sceSaveDataDeleteTransactionResource(tx0) == 0); + const int tx = sceSaveDataCreateTransactionResource(1024); + Require(tx > 0); + Require(sceSaveDataDeleteTransactionResource(tx) == 0); Require(sceSaveDataInitialize3(nullptr) == 0); Require(sceSaveDataTerminate() == 0); SaveDataParam param{}; @@ -307,6 +321,8 @@ int main() { Require(Read(setupBinPath) == expectedBlob512); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); + Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); + Require(sceSaveDataDeleteTransactionResource(tx) == kSaveDataNotInitialized); std::filesystem::current_path(previous); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp index a5d93edc43..32871b4f63 100644 --- a/core/libs/tests/WindowsBacktraceReport.cpp +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -1,11 +1,35 @@ #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" +#include "../prx/libc/include/FailureReport.hpp" extern "C" int* APS5_VABI __error_nid_postfix(); -int main() { +namespace { + +void KeptReport() noexcept { + std::fputs("kept failure report ran\n", stderr); +} + +void DroppedReport() noexcept { + std::fputs("dropped failure report ran\n", stderr); +} + +} + +int main(int argc, char** argv) { if (__error_nid_postfix() == nullptr) return 1; + if (argc == 2 && std::strcmp(argv[1], "report") == 0) { + LibcRegisterFailureReport_nid_no_patch(DroppedReport); + LibcRegisterFailureReport_nid_no_patch(KeptReport); + LibcRegisterFailureReport_nid_no_patch(KeptReport); + LibcUnregisterFailureReport_nid_no_patch(DroppedReport); + } + if (argc == 2 && std::strcmp(argv[1], "report-once") == 0) { + LibcRegisterFailureReport_nid_no_patch(KeptReport); + LibcRunFailureReports_nid_no_patch(); + } _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); std::fputs("aborting\n", stderr); std::abort(); diff --git a/core/libs/tests/WindowsCrashText.cpp b/core/libs/tests/WindowsCrashText.cpp new file mode 100644 index 0000000000..677fbcb609 --- /dev/null +++ b/core/libs/tests/WindowsCrashText.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + SetUnhandledExceptionFilter([](EXCEPTION_POINTERS*) -> LONG { + TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_EXECUTE_HANDLER; + }); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + static const char narrow[] = "allocation failed in the heap"; + static const char16_t wide[] = u"LowLevelFatalError [File:Unknown] [Line: 42] Out of memory"; + static const std::uint8_t binary[] = {0x08, 0xd4, 0x8e, 0x5b, 0xf6, 0x7f, 0x00, 0x00}; + auto* pages = static_cast(VirtualAlloc(nullptr, 0x2000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + DWORD previous = 0; + if (pages == nullptr || !VirtualProtect(pages + 0x1000, 0x1000, PAGE_NOACCESS, &previous)) { + std::fprintf(stderr, "cannot prepare the page edge: error %lu\n", GetLastError()); + return 1; + } + char* const edge = pages + 0x1000 - 8; + std::memcpy(edge, "PAGE END", 8); + std::fputs("faulting\n", stderr); + std::fflush(stderr); + const std::uintptr_t unmapped = 8; + asm volatile( + "movq %0, %%r12\n\t" + "movq %1, %%r13\n\t" + "movq %2, %%r14\n\t" + "movq %3, %%rbx\n\t" + "movq (%4), %%rax" + : + : "r"(narrow), "r"(wide), "r"(edge), "r"(binary), "r"(unmapped) + : "rax", "rbx", "r12", "r13", "r14", "memory"); + std::fputs("the read of address 8 did not fault\n", stderr); + return 1; +} diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 045b6c10b0..2093126aa4 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -170,6 +170,43 @@ void CheckHostWrite() { ::close(descriptors[1]); } +void CheckEmptyRanges() { + Region region(1); + for (const auto offset : {std::size_t{0}, std::size_t{1}, region.page - 1}) { + auto* pointer = region.base + offset; + GuestWriteWatchRegister_nid_postfix(pointer, 0); + Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "an empty registration watches a page"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(1), "a new range is not reported in full"); + + Require(!GuestWriteWatchUnregister_nid_postfix(pointer, 0), "an empty unregister removes a page"); + Require(GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "an empty unregister changes coverage"); + GuestWriteWatchRegister_nid_postfix(pointer, 0); + Require(region.Collect().empty(), "an empty registration marks a watched page fresh"); + + GuestWriteWatchBeginHostWrite_nid_postfix(pointer, 0); + Require(KernelProtection(region.base) == PROT_READ, "an empty host write unprotects a page"); + Require(region.Collect().empty(), "an empty host write reports a dirty page"); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 0); + + GuestWriteWatchBeginHostWrite_nid_postfix(pointer, 1); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 0); + Require(region.Collect() == All(1), "an active host write is not reported"); + Require(KernelProtection(region.base) == (PROT_READ | PROT_WRITE), "an empty end host write releases an active host write"); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 1); + Require(region.Collect() == All(1), "a finished host write is not reported"); + Require(region.Collect().empty(), "a finished host write is reported again"); + + GuestWriteWatchProtectionChanged_nid_postfix(pointer, 0, PROT_NONE); + std::uintptr_t runEnd = 0; + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(1), &runEnd) == (PROT_READ | PROT_WRITE), "an empty protection change changes the recorded protection"); + Require(region.Collect().empty(), "an empty protection change reports a dirty page"); + region.base[offset] = 1; + Require(region.Collect() == All(1), "an empty range operation prevents a later write from being tracked"); + Require(GuestWriteWatchUnregister_nid_postfix(region.base, region.bytes), "cannot unregister the test region"); + } +} + void CheckProtection() { Region region(6); std::uintptr_t runEnd = 0; @@ -232,7 +269,7 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); else if (mode == "fresh") CheckFresh(); @@ -240,6 +277,7 @@ int main(int argc, char** argv) { else if (mode == "register") CheckRegisterAgain(); else if (mode == "mapped") CheckRegisterMapped(); else if (mode == "host") CheckHostWrite(); + else if (mode == "empty") CheckEmptyRanges(); else if (mode == "protection") CheckProtection(); else if (mode == "threads") CheckThreads(); else throw std::runtime_error("unknown write watch test"); diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index 0824ee5009..7e55f2bb41 100644 --- a/core/libs/tests/write_watch/CMakeLists.txt +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(write_watch_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode collect fresh armed register mapped host protection threads) +foreach(mode collect fresh armed register mapped host empty protection threads) add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) endforeach() diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 784fc11985..a0b72b06c9 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -51,6 +51,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -83,6 +84,11 @@ target_compile_features(relinker_core PUBLIC cxx_std_20) set_target_properties(relinker_core PROPERTIES CXX_EXTENSIONS OFF) if(BUILD_TESTING) + add_test_executable(andn_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/AndnLoweringTests.cpp) + target_link_libraries(andn_lowering_tests PRIVATE relinker_core) + add_test(NAME andn_lowering COMMAND andn_lowering_tests) + set_tests_properties(andn_lowering PROPERTIES TIMEOUT 20) + 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) @@ -169,6 +175,7 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -353,6 +360,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp @@ -367,6 +375,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) + if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR (NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))) + target_link_options(amd64_only_converter_tests PRIVATE "LINKER:-pagezero_size,0x10000") + endif() add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) add_test_executable(call_site_resolver_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp @@ -415,6 +426,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + add_test(NAME windows_tls_protected_entries COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_protected_entries.py $) set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15) list(APPEND tlsCoverageGroups displacement_r${register}) @@ -516,6 +529,12 @@ if(BUILD_TESTING) add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) + add_test(NAME guest_module_fini COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_fini.py + $ $) + add_asan_test_options(guest_module_args verify_asan_link_order=0) + add_asan_test_options(guest_deferred_dependencies verify_asan_link_order=0) + add_asan_test_options(guest_module_fini verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) set(windows_stdio_host_arguments) @@ -717,3 +736,9 @@ if(BUILD_TESTING) add_test(NAME reloc_table_bounds COMMAND reloc_table_bounds_tests $) endif() endif() + +if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME macos_guest_weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_imports.py $) + set_property(TEST macos_guest_weak_imports APPEND PROPERTY LABELS relinker) +endif() diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index bd49e3773b..66139266ca 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -10,6 +10,8 @@ struct NativeModules { std::vector Modules; }; +bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second); + 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/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index 7030a03da8..aa54150b47 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -59,10 +60,12 @@ int Autorun(const std::string& absPath, bool toWindows) { cmd += '\''; rawCode = std::system(cmd.c_str()); - if (rawCode != -1) { - const int signal = rawCode & 0x7F; - exitCode = signal ? 128 + signal : (rawCode >> 8) & 0xFF; + if (rawCode == -1) { + const int runError = errno; + throw Domain::RelinkerException("Cannot run or wait for the output executable: " + absPath + " (error " + std::to_string(runError) + ")"); } + const int signal = rawCode & 0x7F; + exitCode = signal ? 128 + signal : (rawCode >> 8) & 0xFF; std::cout << "\nRaw exit code: " << rawCode << "; Unpacked: " << exitCode << '\n'; #endif diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index 4943091c64..f3839033c8 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -7,6 +7,11 @@ namespace Cli { +bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second) { + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return std::filesystem::equivalent(first, second); + return std::filesystem::weakly_canonical(first) == std::filesystem::weakly_canonical(second); +} + 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(); @@ -32,7 +37,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil 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)); + if (std::any_of(directories.begin(), directories.end(), [&](const auto& existing) { return SameDirectory(existing, directory); })) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); directories.push_back(directory); runtimeDirectories.push_back(directory); } diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index 4890de71de..c4883221a5 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -42,6 +44,28 @@ int main(int, char* argv[]) { } } #ifndef _WIN32 + const auto savedHandler = std::signal(SIGCHLD, SIG_IGN); + if (savedHandler == SIG_ERR) throw std::runtime_error("Cannot ignore SIGCHLD for the autorun failure test"); + try { + for (const bool windows : {false, true}) { + bool rejected = false; + try { + Cli::Autorun(child.string(), windows); + } catch (const Domain::RelinkerException& error) { + const std::string message = error.what(); + if (message.find(child.string()) == std::string::npos || + message.find("(error " + std::to_string(ECHILD) + ")") == std::string::npos) + throw std::runtime_error("Autorun lost the failed wait's path or ECHILD error: " + message); + rejected = true; + } + if (!rejected) throw std::runtime_error("Autorun reported success after losing the child exit status"); + } + } catch (...) { + std::signal(SIGCHLD, savedHandler); + throw; + } + if (std::signal(SIGCHLD, savedHandler) == SIG_ERR) + throw std::runtime_error("Cannot restore SIGCHLD after the autorun failure test"); std::filesystem::rename(child, signaled); const int code = Cli::Autorun(signaled.string(), false); if (code != 128 + SIGILL) throw std::runtime_error("Autorun lost the child signal"); diff --git a/core/relinker/codegen/include/codegen/x86/AndnLowering.hpp b/core/relinker/codegen/include/codegen/x86/AndnLowering.hpp new file mode 100644 index 0000000000..00ecfe2cb6 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/AndnLowering.hpp @@ -0,0 +1,22 @@ +#ifndef CODEGEN_X86_ANDNLOWERING_HPP +#define CODEGEN_X86_ANDNLOWERING_HPP + +#include +#include + +namespace Codegen { + +struct AndnOperands { + std::uint8_t Destination; + std::uint8_t InvertedSource; + std::uint8_t Source; + bool Wide; + std::optional Memory; +}; + +[[nodiscard]] std::optional DecodeAndn(const std::uint8_t* data, std::size_t length); +void EmitAndn(StubBodyBuilder& body, const AndnOperands& operands); + +} + +#endif diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 112fed6de7..7b6175bdff 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -217,6 +218,8 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstructio std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const DecodedInstruction& instr, std::span trailing) const { if (_target != Amd64OnlyTarget::Rosetta) return std::nullopt; + if (const auto andn = DecodeAndn(instr.Data, instr.Length)) + return _trampoline("ANDN", instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { EmitAndn(body, *andn); })); if (instr.IsRdseed()) { std::vector replacement(instr.Data, instr.Data + instr.Length); replacement[instr.OpcodeOffset() + 2] &= static_cast(~(1u << X64OpcodeConstants::ModRmRegShift)); @@ -261,6 +264,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c body.Raw(_rdpidReplacement(instr)); return kRdpid.Name; } + if (const auto andn = _target == Amd64OnlyTarget::Rosetta ? DecodeAndn(instr.Data, instr.Length) : std::nullopt) { + EmitAndn(body, *andn); + return "ANDN"; + } if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; diff --git a/core/relinker/codegen/src/x86/AndnLowering.cpp b/core/relinker/codegen/src/x86/AndnLowering.cpp new file mode 100644 index 0000000000..a99b92e4f6 --- /dev/null +++ b/core/relinker/codegen/src/x86/AndnLowering.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::int32_t kFrameSize = 144; +constexpr std::uint8_t kStack = 4; + +bool _prefix(std::uint8_t value) { + return value == 0x2E || value == 0x36 || value == 0x3E || value == 0x26 || value == 0x64 || value == 0x65 || value == 0x67; +} + +void _append32(std::vector& bytes, std::int32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) + bytes.push_back(static_cast(static_cast(value) >> shift)); +} + +std::uint8_t _rex(bool wide, std::uint8_t reg, std::uint8_t rm) { + return static_cast(0x40 | (wide ? 8 : 0) | ((reg & 8) >> 1) | ((rm & 8) >> 3)); +} + +void _register(StubBodyBuilder& body, std::uint8_t opcode, bool wide, std::uint8_t reg, std::uint8_t rm) { + const std::array bytes{_rex(wide, reg, rm), opcode, static_cast(0xC0 | ((reg & 7) << 3) | (rm & 7))}; + body.Raw(bytes); +} + +void _stack(StubBodyBuilder& body, std::uint8_t opcode, bool wide, std::uint8_t reg, std::int32_t displacement) { + std::vector bytes{_rex(wide, reg, kStack), opcode, static_cast(0x84 | ((reg & 7) << 3)), 0x24}; + _append32(bytes, displacement); + body.Raw(bytes); +} + +std::uint8_t _scratch(const AndnOperands& operands) { + std::array used{}; + used[kStack] = used[operands.Destination] = used[operands.InvertedSource] = true; + if (!operands.Memory) { + used[operands.Source] = true; + } else { + const auto& memory = *operands.Memory; + if (memory.Rm == 4) { + const auto base = static_cast((memory.Sib & 7) | ((memory.RexIndexBase & 1) << 3)); + const auto index = static_cast(((memory.Sib >> 3) & 7) | ((memory.RexIndexBase & 2) << 2)); + if (memory.Mod != 0 || (memory.Sib & 7) != 5) used[base] = true; + if (index != kStack) used[index] = true; + } else if (!memory.RipRelative) { + used[memory.Rm | ((memory.RexIndexBase & 1) << 3)] = true; + } + } + for (const auto reg : {11, 10, 9, 8, 0, 1, 2, 3, 5, 6, 7, 12, 13, 14, 15}) + if (!used[reg]) return static_cast(reg); + throw CodegenException("ANDN has no scratch register"); +} + +void _memory(StubBodyBuilder& body, const AndnOperands& operands, std::uint8_t scratch) { + const auto& memory = *operands.Memory; + auto displacement = static_cast(memory.Displacement); + if (memory.StackBase) displacement += kFrameSize; + const bool address32 = std::find(memory.Prefixes.begin(), memory.Prefixes.end(), 0x67) != memory.Prefixes.end(); + if (!address32 && displacement > std::numeric_limits::max()) + throw CodegenException("ANDN stack displacement does not fit after spilling"); + const auto mod = memory.StackBase ? 2 : memory.Mod; + auto bytes = memory.Prefixes; + bytes.push_back(static_cast(_rex(operands.Wide, scratch, 0) | memory.RexIndexBase)); + bytes.push_back(0x23); + bytes.push_back(static_cast((mod << 6) | ((scratch & 7) << 3) | memory.Rm)); + if (memory.Rm == 4) bytes.push_back(memory.Sib); + const auto offset = bytes.size(); + if (mod == 1) bytes.push_back(static_cast(displacement)); + if (mod == 2 || (mod == 0 && (memory.RipRelative || (memory.Rm == 4 && (memory.Sib & 7) == 5)))) + _append32(bytes, static_cast(displacement)); + if (memory.RipRelative) + body.RipRelative(bytes, offset, static_cast(memory.InstructionLength) + memory.Displacement); + else + body.Raw(bytes); +} + +} + +std::optional DecodeAndn(const std::uint8_t* data, const std::size_t length) { + std::size_t vex = 0; + while (vex < length && _prefix(data[vex])) ++vex; + if (vex + 5 > length || data[vex] != 0xC4 || (data[vex + 1] & 0x1F) != 2 || data[vex + 3] != 0xF2) + return std::nullopt; + if ((data[vex + 2] & 7) != 0) + throw CodegenException("ANDN requires VEX.L=0 and VEX.pp=0"); + const auto extensions = static_cast(~data[vex + 1]); + const auto modrm = data[vex + 4]; + AndnOperands operands{ + static_cast(((modrm >> 3) & 7) | ((extensions & 0x80) >> 4)), + static_cast((~data[vex + 2] >> 3) & 15), + static_cast((modrm & 7) | ((extensions & 0x20) >> 2)), + (data[vex + 2] & 0x80) != 0, std::nullopt}; + if ((modrm >> 6) == 3) { + if (vex + 5 != length) throw CodegenException("ANDN register operand has trailing bytes"); + } else { + const auto rex = static_cast(((extensions & 0x20) >> 5) | ((extensions & 0x40) >> 5)); + operands.Memory = DecodeMemoryOperand(data, length, vex + 4, rex, {data, data + vex}); + if (vex + 4 + operands.Memory->EncodedSize != length) + throw CodegenException("ANDN memory operand has trailing bytes"); + } + return operands; +} + +void EmitAndn(StubBodyBuilder& body, const AndnOperands& operands) { + const auto scratch = _scratch(operands); + _stack(body, 0x8D, true, kStack, -kFrameSize); + _stack(body, 0x89, true, scratch, 0); + if (!operands.Memory && operands.Source == kStack) { + _stack(body, 0x8D, true, scratch, kFrameSize); + _stack(body, 0x89, true, scratch, 8); + } + if (operands.InvertedSource == kStack) + _stack(body, 0x8D, true, scratch, kFrameSize); + else + _register(body, 0x8B, operands.Wide, scratch, operands.InvertedSource); + _register(body, 0xF7, operands.Wide, 2, scratch); + if (operands.Memory) + _memory(body, operands, scratch); + else if (operands.Source == kStack) + _stack(body, 0x23, operands.Wide, scratch, 8); + else + _register(body, 0x23, operands.Wide, scratch, operands.Source); + if (operands.Destination == kStack) + _stack(body, 0x89, operands.Wide, scratch, 8); + else + _register(body, 0x8B, operands.Wide, operands.Destination, scratch); + _stack(body, 0x8B, true, scratch, 0); + _stack(body, 0x8D, true, kStack, kFrameSize); + if (operands.Destination == kStack) + _stack(body, 0x8B, operands.Wide, kStack, 8 - kFrameSize); +} + +} diff --git a/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp index c0c31a92e6..0f1e217d5c 100644 --- a/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp +++ b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp @@ -32,7 +32,23 @@ std::optional DecodeScalarCompare(const std::uint8_t* dat return std::nullopt; modRmOffset = pos + 4; } else { - return std::nullopt; + bool singlePrecision = false; + bool doublePrecision = false; + for (; pos < length; ++pos) { + if (data[pos] == PrefixRep) + singlePrecision = true; + else if (data[pos] == PrefixRepne) + doublePrecision = true; + else if (!_segmentPrefix(data[pos])) + break; + } + if (singlePrecision == doublePrecision) + return std::nullopt; + if (pos < length && data[pos] >= RexMin && data[pos] <= RexMax) + ++pos; + if (pos + 3 > length || data[pos] != TwoByteOpcodeEscape || data[pos + 1] != kCompareOpcode) + return std::nullopt; + modRmOffset = pos + 2; } const auto modrm = data[modRmOffset]; const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index f905d2df43..a3f7f3962d 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -488,12 +488,12 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( } return info; } - if (reg == 3) { - info.FlowKind = ControlFlowKind::IndirectCall; - return info; - } - if (reg == 5) { - info.FlowKind = ControlFlowKind::IndirectJump; + if (reg == 3 || reg == 5) { + if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { + info.HasRipRelativeDisp = true; + info.RipRelativeDispOffset = pos + 1; + } + info.FlowKind = reg == 3 ? ControlFlowKind::IndirectCall : ControlFlowKind::IndirectJump; return info; } } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index dcb82e697d..b88e1323d8 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -24,7 +24,7 @@ #include #include #include -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) #include #endif #include @@ -969,7 +969,7 @@ void rosettaScalarCompares() { require(vex3 && Bytes(vex3->StubBody.begin(), vex3->StubBody.begin() + 6) == Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x03}, "A VEX3 VCMPSD with a hazard predicate does not move unchanged"); for (const Bytes& kept : {Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x00}, Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x01}, Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x02}, Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x01}}) require(!match(kept).has_value(), "An EQ_OQ, LT_OS or LE_OS scalar compare must stay inline"); - for (const Bytes& other : {Bytes{0xC5, 0xE9, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xE8, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xEA, 0x2E, 0xC8}, Bytes{0xC5, 0xEA, 0xC2, 0xCA}, Bytes{0xC4, 0xE2, 0xEB, 0xC2, 0xCB, 0x03}, Bytes{0x62, 0xD1, 0x76, 0x08, 0xC2, 0xCB, 0x03}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x03}}) + for (const Bytes& other : {Bytes{0xC5, 0xE9, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xE8, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xEA, 0x2E, 0xC8}, Bytes{0xC5, 0xEA, 0xC2, 0xCA}, Bytes{0xC4, 0xE2, 0xEB, 0xC2, 0xCB, 0x03}, Bytes{0x62, 0xD1, 0x76, 0x08, 0xC2, 0xCB, 0x03}}) require(!match(other).has_value(), "An instruction other than a Rosetta-hazard scalar compare was moved"); require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(compare.data(), compare.size()).has_value(), "A scalar compare was moved for an Intel host"); const std::vector> pair = {std::span(compare), std::span(rip)}; @@ -977,6 +977,48 @@ void rosettaScalarCompares() { require(sequence && sequence->InstructionName == "scalar compare" && sequence->ReturnBranchOffset == 14 && sequence->Relocations.size() == 1 && sequence->Relocations[0].SiteTarget == 5 + 9 + 0x100, "A scalar compare after another is not emitted out of line in one stub"); } +void rosettaLegacyScalarCompares() { + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const auto match = [&](const Bytes& bytes) { return rosetta->Match(bytes.data(), bytes.size()); }; + for (const Bytes& moved : {Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0xF2, 0x0F, 0xC2, 0xCA, 0x04}, Bytes{0xF2, 0x41, 0x0F, 0xC2, 0xC9, 0x05}, + Bytes{0x64, 0xF3, 0x0F, 0xC2, 0x4F, 0x68, 0x06}, Bytes{0xF3, 0x2E, 0x0F, 0xC2, 0x44, 0x24, 0x08, 0x07}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x08}}) { + const auto lowered = match(moved); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && lowered->Optional && lowered->InstructionName == "scalar compare", "A legacy Rosetta-hazard scalar compare was not lowered through an optional stub"); + require(lowered->ReturnBranchOffset == moved.size() && lowered->StubBody.size() == moved.size() + 5 && Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(moved.size())) == moved && lowered->StubBody[moved.size()] == 0xE9 && lowered->Relocations.empty(), "A legacy scalar compare does not run unchanged before its return jump"); + } + const auto rip = match(Bytes{0xF3, 0x0F, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x06}); + require(rip && rip->Relocations.size() == 1 && rip->Relocations[0].DisplacementOffset == 4 && rip->Relocations[0].InstructionEnd == 9 && rip->Relocations[0].SiteTarget == 9 + 0x100, "A RIP-relative legacy CMPSS was not relocated from its place in the site"); + const auto ripRex = match(Bytes{0xF2, 0x48, 0x0F, 0xC2, 0x05, 0x00, 0x01, 0x00, 0x00, 0x07}); + require(ripRex && ripRex->Relocations.size() == 1 && ripRex->Relocations[0].DisplacementOffset == 5 && ripRex->Relocations[0].InstructionEnd == 10 && ripRex->Relocations[0].SiteTarget == 10 + 0x100, "A RIP-relative legacy CMPSD with REX was not relocated"); + for (const Bytes& kept : {Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x00}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x01}, Bytes{0xF2, 0x0F, 0xC2, 0xCA, 0x02}}) + require(!match(kept).has_value(), "A legacy EQ_OQ, LT_OS or LE_OS scalar compare must stay inline"); + for (const Bytes& other : {Bytes{0x0F, 0xC2, 0xCA, 0x03}, Bytes{0x66, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0x66, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0xF2, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, + Bytes{0x67, 0xF3, 0x0F, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03}, Bytes{0xF0, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0x48, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, + Bytes{0xF3, 0x0F, 0xC2, 0xCA}, Bytes{0xF3, 0x0F, 0xC3, 0xCA, 0x03}, Bytes{0xF3, 0x0F, 0x10, 0xCA}}) + require(!match(other).has_value(), "An instruction other than a legacy Rosetta-hazard scalar compare was moved"); + const Bytes compare = {0xF3, 0x0F, 0xC2, 0xCA, 0x03}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(compare.data(), compare.size()).has_value(), "A legacy scalar compare was moved for an Intel host"); +} + +void converterRosettaLegacyScalarCompare() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes file(0x300, 0xCC); + const Bytes text = { + 0xF3, 0x0F, 0x10, 0x15, 0x10, 0x00, 0x00, 0x00, + 0x0F, 0x28, 0xC2, + 0xF3, 0x0F, 0xC2, 0xCA, 0x03, + 0xC5, 0xF8, 0x28, 0x0D, 0x20, 0x00, 0x00, 0x00, + 0xC5, 0xF8, 0x2E, 0xC8, + 0xC3}; + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.Trampolines.size() == 1 && result.Reports.size() == 1 && result.Reports[0].InstructionName == "scalar compare" && result.Reports[0].Offset == 0x20B && result.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Trampoline, "The legacy compare in the fmax shape was not the only moved instruction"); + const auto& site = result.Trampolines[0]; + require(site.Offset == 0x20B && site.Length == 5 && Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes(text.begin() + 11, text.begin() + 16) && site.Relocations.empty(), "The legacy compare stub does not run the compare unchanged"); + const auto intel = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segmentHeader(text.size())}); + require(intel.Reports.empty() && intel.Bytes == file, "The Intel target moved a legacy scalar compare"); +} + void converterRosettaScalarCompare() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -1247,7 +1289,7 @@ void linuxRipRelativePlacement() { } -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) std::uint64_t extrqReference(std::uint64_t value, std::uint64_t control) { const auto length = static_cast(control & 0x3f); const auto index = static_cast((control >> 8) & 0x3f); @@ -1729,6 +1771,8 @@ int main() { converterRosetta(); rosettaScalarCompares(); converterRosettaScalarCompare(); + rosettaLegacyScalarCompares(); + converterRosettaLegacyScalarCompare(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); diff --git a/core/relinker/codegen/tests/AndnLoweringTests.cpp b/core/relinker/codegen/tests/AndnLoweringTests.cpp new file mode 100644 index 0000000000..6925e2e0b5 --- /dev/null +++ b/core/relinker/codegen/tests/AndnLoweringTests.cpp @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) +#include +#endif + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +Bytes instruction(bool wide, std::uint8_t dst, std::uint8_t first, std::uint8_t second) { + return {0xC4, static_cast(0xE2 ^ ((dst & 8) << 4) ^ ((second & 8) << 2)), + static_cast((wide ? 0x80 : 0) | ((~first & 15) << 3)), 0xF2, + static_cast(0xC0 | ((dst & 7) << 3) | (second & 7))}; +} + +void decoding() { + for (bool wide : {false, true}) { + for (std::uint8_t dst = 0; dst < 16; ++dst) { + for (std::uint8_t first = 0; first < 16; ++first) { + for (std::uint8_t second = 0; second < 16; ++second) { + const auto bytes = instruction(wide, dst, first, second); + const auto operands = Codegen::DecodeAndn(bytes.data(), bytes.size()); + require(operands && operands->Wide == wide && operands->Destination == dst && operands->InvertedSource == first && operands->Source == second && !operands->Memory, "ANDN register decoding changed an operand"); + } + } + } + } + const Bytes memory{0x64, 0x67, 0xC4, 0x02, 0xA8, 0xF2, 0x8C, 0xAC, 0x10, 0, 0, 0}; + const auto operands = Codegen::DecodeAndn(memory.data(), memory.size()); + require(operands && operands->Destination == 9 && operands->InvertedSource == 10 && operands->Memory && operands->Memory->RexIndexBase == 3 && operands->Memory->Prefixes == Bytes{0x64, 0x67}, "ANDN memory extensions or prefixes were lost"); + for (const Bytes bytes : {Bytes{0x90}, Bytes{0xC4, 0xE2, 0xF0}, Bytes{0xC4, 0xE1, 0xF0, 0xF2, 0xC0}}) + require(!Codegen::DecodeAndn(bytes.data(), bytes.size()), "Unrelated or truncated opcode matched ANDN"); + for (const Bytes bytes : {Bytes{0xC4, 0xE2, 0xF4, 0xF2, 0xC0}, Bytes{0xC4, 0xE2, 0xF1, 0xF2, 0xC0}, Bytes{0xC4, 0xE2, 0xF0, 0xF2, 0x84}}) { + bool rejected = false; + try { (void)Codegen::DecodeAndn(bytes.data(), bytes.size()); } + catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected, "Malformed ANDN was accepted"); + } + const Bytes overflow{0xC4, 0xE2, 0xF0, 0xF2, 0x84, 0x24, 0xFF, 0xFF, 0xFF, 0x7F}; + bool rejected = false; + try { + Codegen::StubBodyBuilder body; + Codegen::EmitAndn(body, *Codegen::DecodeAndn(overflow.data(), overflow.size())); + } catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected, "An overflowing stack displacement was silently wrapped"); +} + +void matching() { + const auto bytes = instruction(true, 0, 1, 6); + const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + require(!intel->Match(bytes.data(), bytes.size()), "Intel mode rewrote ANDN"); + const auto match = rosetta->Match(bytes.data(), bytes.size()); + require(match && match->InstructionName == "ANDN" && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !match->Optional, "Rosetta mode did not require an ANDN trampoline"); + const std::array, 2> sequence{bytes, bytes}; + require(rosetta->MatchSequence(sequence, {}).has_value(), "An ANDN sequence could not move out of line"); +} + +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) + +struct State { + std::array Input{}; + std::array Output{}; + std::uint64_t Flags = 0; + std::array RedZone{}; +}; + +class Execution { +public: + Execution(const Bytes& site, std::uint64_t memoryValue) { + auto operands = Codegen::DecodeAndn(site.data(), site.size()); + require(operands.has_value(), "Execution site was not ANDN"); + for (auto reg : {3, 5, 12, 13, 14, 15}) push(static_cast(reg)); + memory(0x8D, 4, 4, -128); + rip({0x48, 0x89, 0x3D}, 0); + rip({0x48, 0x89, 0x25}, 8); + memory(0x89, 4, 7, 32); + bytes.insert(bytes.end(), {0x48, 0xB8}); + append64(kSentinel); + for (int displacement = -128; displacement < 0; displacement += 8) + memory(0x89, 0, 4, displacement); + memory(0x8B, 0, 7, 16 * 8); + memory(0x89, 0, 4, 64); + for (std::uint8_t reg = 0; reg < 16; ++reg) + if (reg != 4 && reg != 7) memory(0x8B, reg, 7, reg * 8); + memory(0x8B, 7, 7, 7 * 8); + const auto siteOffset = bytes.size(); + if (operands->Memory && operands->Memory->RipRelative) { + auto relocated = site; + const auto displacement = static_cast(4096 + 24 - (siteOffset + site.size())); + std::memcpy(relocated.data() + relocated.size() - 4, &displacement, sizeof(displacement)); + operands = Codegen::DecodeAndn(relocated.data(), relocated.size()); + } + Codegen::StubBodyBuilder builder; + Codegen::EmitAndn(builder, *operands); + auto body = builder.Finish(); + const auto stubOffset = bytes.size(); + Codegen::ApplyStubRelocations(body.Bytes, body.Relocations, siteOffset, stubOffset, 0); + bytes.insert(bytes.end(), body.Bytes.begin(), body.Bytes.begin() + static_cast(body.ReturnBranchOffset)); + rip({0x48, 0x89, 0x25}, 16); + rip({0x48, 0x8B, 0x25}, 8); + memory(0x8D, 4, 4, -256); + for (std::uint8_t reg = 0; reg < 16; ++reg) if (reg != 4) push(reg); + bytes.push_back(0x9C); + rip({0x48, 0x8B, 0x3D}, 0); + memory(0x8B, 0, 4, 0); + memory(0x89, 0, 7, 32 * 8); + int offset = 8; + for (int reg = 15; reg >= 0; --reg) { + if (reg == 4) continue; + memory(0x8B, 0, 4, offset); + memory(0x89, 0, 7, (16 + reg) * 8); + offset += 8; + } + rip({0x48, 0x8B, 0x05}, 16); + memory(0x89, 0, 7, 20 * 8); + for (int slot = 0; slot < 16; ++slot) { + memory(0x8B, 0, 4, 256 + 128 - 128 + slot * 8); + memory(0x89, 0, 7, (33 + slot) * 8); + } + rip({0x48, 0x8B, 0x25}, 8); + memory(0x8D, 4, 4, 128); + for (auto reg : {15, 14, 13, 12, 5, 3}) pop(static_cast(reg)); + bytes.push_back(0xC3); + require(bytes.size() < 4096, "Execution wrapper exceeded its code page"); + code = static_cast(mmap(nullptr, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + require(code != MAP_FAILED, "Cannot allocate execution test pages"); + std::memcpy(code, bytes.data(), bytes.size()); + std::memcpy(code + 4096 + 24, &memoryValue, sizeof(memoryValue)); + require(mprotect(code, 4096, PROT_READ | PROT_EXEC) == 0, "Cannot make ANDN test code executable"); + } + + ~Execution() { if (code != MAP_FAILED) munmap(code, 8192); } + + void Run(State& state) const { reinterpret_cast(code)(&state); } + const void* Memory() const { return code + 4096 + 24; } + static constexpr std::uint64_t kSentinel = 0xBADC0FFEE0DDF00Dull; + +private: + Bytes bytes; + std::uint8_t* code = reinterpret_cast(MAP_FAILED); + + void append32(std::int32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast(static_cast(value) >> shift)); + } + void append64(std::uint64_t value) { + for (unsigned shift = 0; shift < 64; shift += 8) bytes.push_back(static_cast(value >> shift)); + } + void rip(std::initializer_list opcode, int slot) { + bytes.insert(bytes.end(), opcode); + append32(static_cast(4096 + slot - (bytes.size() + 4))); + } + void push(std::uint8_t reg) { + if (reg >= 8) bytes.push_back(0x41); + bytes.push_back(static_cast(0x50 | (reg & 7))); + } + void pop(std::uint8_t reg) { + if (reg >= 8) bytes.push_back(0x41); + bytes.push_back(static_cast(0x58 | (reg & 7))); + } + void memory(std::uint8_t opcode, std::uint8_t reg, std::uint8_t base, int displacement) { + bytes.insert(bytes.end(), {static_cast(0x48 | ((reg & 8) >> 1) | ((base & 8) >> 3)), opcode, + static_cast(0x80 | ((reg & 7) << 3) | (base & 7))}); + if ((base & 7) == 4) bytes.push_back(0x24); + append32(displacement); + } +}; + +void check(const State& state, const Codegen::AndnOperands& operands, std::uint64_t second) { + const auto mask = operands.Wide ? ~std::uint64_t{0} : 0xFFFFFFFFull; + const auto expected = (~state.Input[operands.InvertedSource] & second) & mask; + for (std::size_t reg = 0; reg < 16; ++reg) + require(state.Output[reg] == (reg == operands.Destination ? expected : state.Input[reg]), "ANDN changed a source/scratch register or produced an incorrect result"); + const auto flags = (expected == 0 ? 0x40u : 0u) | ((expected >> (operands.Wide ? 63 : 31)) != 0 ? 0x80u : 0u); + require((state.Flags & 0x8C1u) == flags, "ANDN did not preserve its defined flag semantics"); + for (const auto value : state.RedZone) require(value == Execution::kSentinel, "ANDN overwrote the caller's red zone"); +} + +State initialState() { + State state; + for (std::size_t reg = 0; reg < 16; ++reg) state.Input[reg] = 0xABCD123456789000ull + reg * 0x1234567ull; + return state; +} + +void execution() { + for (bool wide : {false, true}) { + for (std::uint8_t dst = 0; dst < 16; ++dst) { + for (const auto pair : {std::pair{1, 6}, {dst, 10}, {11, dst}, {dst, dst}, {4, 9}, {9, 4}}) { + const auto site = instruction(wide, dst, pair.first, pair.second); + Execution executable(site, 0); + for (int variant = 0; variant < 3; ++variant) { + auto state = initialState(); + if (variant == 1) state.Input.fill(0); + if (variant == 2) state.Input.fill(~std::uint64_t{0}); + executable.Run(state); + check(state, *Codegen::DecodeAndn(site.data(), site.size()), state.Input[pair.second]); + } + } + } + for (const Bytes tail : {Bytes{0x06}, Bytes{0x46, 0x10}, Bytes{0x84, 0x24, 0x40, 0, 0, 0}, Bytes{0x04, 0xCA}, Bytes{0x85, 0x10, 0, 0, 0}, Bytes{0x05, 0, 0, 0, 0}}) { + Bytes site{0xC4, 0xE2, static_cast(wide ? 0xF0 : 0x70), 0xF2}; + site.insert(site.end(), tail.begin(), tail.end()); + constexpr std::uint64_t value = 0xFEDCBA9876543210ull; + Execution executable(site, value); + auto state = initialState(); + state.Input[6] = reinterpret_cast(executable.Memory()) - (tail[0] == 0x46 ? 16 : 0); + state.Input[2] = reinterpret_cast(executable.Memory()) - 3 * 8; + state.Input[1] = 3; + state.Input[5] = reinterpret_cast(executable.Memory()) - 16; + state.Output[0] = value; + executable.Run(state); + check(state, *Codegen::DecodeAndn(site.data(), site.size()), value); + } + } +} +#else +void execution() { std::cout << "ANDN execution checks require a Linux/macOS x86-64 build\n"; } +#endif + +} + +int main() { + try { + decoding(); + matching(); + execution(); + std::cout << "ANDN lowering checks passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index e141e1539e..54dd90df2b 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -1,4 +1,5 @@ #include "DecoderLengthCases.hpp" +#include "DecoderMetadataCases.hpp" #include #include #include @@ -77,6 +78,27 @@ int main() { } catch (const Codegen::CodegenException&) { } } + for (const auto& testCase : kDecoderRipRelativeCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "RIP-relative case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + require(decodedLength(decoder, bytes, name) == testCase.Length, "Wrong length for " + name); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.HasRipRelativeDisp, "RIP-relative operand of " + name + " was not reported"); + require(info.RipRelativeDispOffset == testCase.DisplacementOffset, "Wrong RIP-relative displacement offset for " + name); + } + for (const auto& testCase : kDecoderBranchCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Branch case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.Length == testCase.Length, "Wrong length for " + name); + require(info.FlowKind == testCase.FlowKind, "Wrong control flow kind for " + name); + require(info.HasBranchTarget == testCase.HasBranchTarget, "Wrong branch target presence for " + name); + require(!testCase.HasBranchTarget || info.BranchDisp == testCase.BranchDisp, "Wrong branch displacement for " + name); + } const auto threeDNow = parse("0f0f0d11223344a6"); const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); diff --git a/core/relinker/codegen/tests/DecoderMetadataCases.hpp b/core/relinker/codegen/tests/DecoderMetadataCases.hpp new file mode 100644 index 0000000000..c685b184ea --- /dev/null +++ b/core/relinker/codegen/tests/DecoderMetadataCases.hpp @@ -0,0 +1,792 @@ +#ifndef CODEGEN_TESTS_DECODERMETADATACASES_HPP +#define CODEGEN_TESTS_DECODERMETADATACASES_HPP + +#include +#include +#include + +struct DecoderRipRelativeCase { + const char* Bytes; + std::size_t Length; + std::size_t DisplacementOffset; +}; + +struct DecoderBranchCase { + const char* Bytes; + std::size_t Length; + Codegen::ControlFlowKind FlowKind; + bool HasBranchTarget; + std::int64_t BranchDisp; +}; + +inline constexpr DecoderRipRelativeCase kDecoderRipRelativeCases[] = { + {"ff0511111111", 6, 2}, + {"ff0d11111111", 6, 2}, + {"ff1511111111", 6, 2}, + {"ff1d11111111", 6, 2}, + {"ff2511111111", 6, 2}, + {"ff2d11111111", 6, 2}, + {"ff3511111111", 6, 2}, + {"000511111111", 6, 2}, + {"010511111111", 6, 2}, + {"020511111111", 6, 2}, + {"030511111111", 6, 2}, + {"080511111111", 6, 2}, + {"090511111111", 6, 2}, + {"0a0511111111", 6, 2}, + {"0b0511111111", 6, 2}, + {"100511111111", 6, 2}, + {"110511111111", 6, 2}, + {"120511111111", 6, 2}, + {"130511111111", 6, 2}, + {"180511111111", 6, 2}, + {"190511111111", 6, 2}, + {"1a0511111111", 6, 2}, + {"1b0511111111", 6, 2}, + {"200511111111", 6, 2}, + {"210511111111", 6, 2}, + {"220511111111", 6, 2}, + {"230511111111", 6, 2}, + {"280511111111", 6, 2}, + {"290511111111", 6, 2}, + {"2a0511111111", 6, 2}, + {"2b0511111111", 6, 2}, + {"300511111111", 6, 2}, + {"310511111111", 6, 2}, + {"320511111111", 6, 2}, + {"330511111111", 6, 2}, + {"380511111111", 6, 2}, + {"390511111111", 6, 2}, + {"3a0511111111", 6, 2}, + {"3b0511111111", 6, 2}, + {"630511111111", 6, 2}, + {"666905111111111111", 9, 3}, + {"69051111111111111111", 10, 2}, + {"6b051111111111", 7, 2}, + {"80051111111111", 7, 2}, + {"668105111111111111", 9, 3}, + {"81051111111111111111", 10, 2}, + {"83051111111111", 7, 2}, + {"840511111111", 6, 2}, + {"850511111111", 6, 2}, + {"860511111111", 6, 2}, + {"870511111111", 6, 2}, + {"880511111111", 6, 2}, + {"890511111111", 6, 2}, + {"8a0511111111", 6, 2}, + {"8b0511111111", 6, 2}, + {"8c0511111111", 6, 2}, + {"8d0511111111", 6, 2}, + {"8e0511111111", 6, 2}, + {"c0051111111111", 7, 2}, + {"c1051111111111", 7, 2}, + {"c6051111111111", 7, 2}, + {"66c705111111111111", 9, 3}, + {"c7051111111111111111", 10, 2}, + {"d00511111111", 6, 2}, + {"d10511111111", 6, 2}, + {"d20511111111", 6, 2}, + {"d30511111111", 6, 2}, + {"d80511111111", 6, 2}, + {"d90511111111", 6, 2}, + {"da0511111111", 6, 2}, + {"db0511111111", 6, 2}, + {"dc0511111111", 6, 2}, + {"dd0511111111", 6, 2}, + {"de0511111111", 6, 2}, + {"df0511111111", 6, 2}, + {"f61511111111", 6, 2}, + {"f6051111111111", 7, 2}, + {"f71511111111", 6, 2}, + {"66f705111111111111", 9, 3}, + {"f7051111111111111111", 10, 2}, + {"fe0511111111", 6, 2}, + {"0f000511111111", 7, 3}, + {"0f010511111111", 7, 3}, + {"0f020511111111", 7, 3}, + {"0f030511111111", 7, 3}, + {"0f0d0511111111", 7, 3}, + {"0f100511111111", 7, 3}, + {"0f110511111111", 7, 3}, + {"0f120511111111", 7, 3}, + {"0f130511111111", 7, 3}, + {"0f140511111111", 7, 3}, + {"0f150511111111", 7, 3}, + {"0f160511111111", 7, 3}, + {"0f170511111111", 7, 3}, + {"0f180511111111", 7, 3}, + {"0f190511111111", 7, 3}, + {"0f1a0511111111", 7, 3}, + {"0f1b0511111111", 7, 3}, + {"0f1c0511111111", 7, 3}, + {"0f1d0511111111", 7, 3}, + {"0f1e0511111111", 7, 3}, + {"0f1f0511111111", 7, 3}, + {"0f280511111111", 7, 3}, + {"0f290511111111", 7, 3}, + {"0f2a0511111111", 7, 3}, + {"0f2b0511111111", 7, 3}, + {"0f2c0511111111", 7, 3}, + {"0f2d0511111111", 7, 3}, + {"0f2e0511111111", 7, 3}, + {"0f2f0511111111", 7, 3}, + {"0f400511111111", 7, 3}, + {"0f410511111111", 7, 3}, + {"0f420511111111", 7, 3}, + {"0f430511111111", 7, 3}, + {"0f440511111111", 7, 3}, + {"0f450511111111", 7, 3}, + {"0f460511111111", 7, 3}, + {"0f470511111111", 7, 3}, + {"0f480511111111", 7, 3}, + {"0f490511111111", 7, 3}, + {"0f4a0511111111", 7, 3}, + {"0f4b0511111111", 7, 3}, + {"0f4c0511111111", 7, 3}, + {"0f4d0511111111", 7, 3}, + {"0f4e0511111111", 7, 3}, + {"0f4f0511111111", 7, 3}, + {"0f510511111111", 7, 3}, + {"0f520511111111", 7, 3}, + {"0f530511111111", 7, 3}, + {"0f540511111111", 7, 3}, + {"0f550511111111", 7, 3}, + {"0f560511111111", 7, 3}, + {"0f570511111111", 7, 3}, + {"0f580511111111", 7, 3}, + {"0f590511111111", 7, 3}, + {"0f5a0511111111", 7, 3}, + {"0f5b0511111111", 7, 3}, + {"0f5c0511111111", 7, 3}, + {"0f5d0511111111", 7, 3}, + {"0f5e0511111111", 7, 3}, + {"0f5f0511111111", 7, 3}, + {"0f600511111111", 7, 3}, + {"0f610511111111", 7, 3}, + {"0f620511111111", 7, 3}, + {"0f630511111111", 7, 3}, + {"0f640511111111", 7, 3}, + {"0f650511111111", 7, 3}, + {"0f660511111111", 7, 3}, + {"0f670511111111", 7, 3}, + {"0f680511111111", 7, 3}, + {"0f690511111111", 7, 3}, + {"0f6a0511111111", 7, 3}, + {"0f6b0511111111", 7, 3}, + {"660f6c0511111111", 8, 4}, + {"660f6d0511111111", 8, 4}, + {"0f6e0511111111", 7, 3}, + {"0f6f0511111111", 7, 3}, + {"0f70051111111111", 8, 3}, + {"0f740511111111", 7, 3}, + {"0f750511111111", 7, 3}, + {"0f760511111111", 7, 3}, + {"660f7c0511111111", 8, 4}, + {"660f7d0511111111", 8, 4}, + {"0f7e0511111111", 7, 3}, + {"0f7f0511111111", 7, 3}, + {"0f900511111111", 7, 3}, + {"0f910511111111", 7, 3}, + {"0f920511111111", 7, 3}, + {"0f930511111111", 7, 3}, + {"0f940511111111", 7, 3}, + {"0f950511111111", 7, 3}, + {"0f960511111111", 7, 3}, + {"0f970511111111", 7, 3}, + {"0f980511111111", 7, 3}, + {"0f990511111111", 7, 3}, + {"0f9a0511111111", 7, 3}, + {"0f9b0511111111", 7, 3}, + {"0f9c0511111111", 7, 3}, + {"0f9d0511111111", 7, 3}, + {"0f9e0511111111", 7, 3}, + {"0f9f0511111111", 7, 3}, + {"0fa30511111111", 7, 3}, + {"0fa4051111111111", 8, 3}, + {"0fa50511111111", 7, 3}, + {"0fab0511111111", 7, 3}, + {"0fac051111111111", 8, 3}, + {"0fad0511111111", 7, 3}, + {"0fae0511111111", 7, 3}, + {"0faf0511111111", 7, 3}, + {"0fb00511111111", 7, 3}, + {"0fb10511111111", 7, 3}, + {"0fb20511111111", 7, 3}, + {"0fb30511111111", 7, 3}, + {"0fb40511111111", 7, 3}, + {"0fb50511111111", 7, 3}, + {"0fb60511111111", 7, 3}, + {"0fb70511111111", 7, 3}, + {"f30fb80511111111", 8, 4}, + {"0fb90511111111", 7, 3}, + {"0fba251111111111", 8, 3}, + {"0fbb0511111111", 7, 3}, + {"0fbc0511111111", 7, 3}, + {"0fbd0511111111", 7, 3}, + {"0fbe0511111111", 7, 3}, + {"0fbf0511111111", 7, 3}, + {"0fc00511111111", 7, 3}, + {"0fc10511111111", 7, 3}, + {"0fc2051111111111", 8, 3}, + {"0fc30511111111", 7, 3}, + {"0fc4051111111111", 8, 3}, + {"0fc6051111111111", 8, 3}, + {"0fc70d11111111", 7, 3}, + {"660fd00511111111", 8, 4}, + {"0fd10511111111", 7, 3}, + {"0fd20511111111", 7, 3}, + {"0fd30511111111", 7, 3}, + {"0fd40511111111", 7, 3}, + {"0fd50511111111", 7, 3}, + {"660fd60511111111", 8, 4}, + {"0fd80511111111", 7, 3}, + {"0fd90511111111", 7, 3}, + {"0fda0511111111", 7, 3}, + {"0fdb0511111111", 7, 3}, + {"0fdc0511111111", 7, 3}, + {"0fdd0511111111", 7, 3}, + {"0fde0511111111", 7, 3}, + {"0fdf0511111111", 7, 3}, + {"0fe00511111111", 7, 3}, + {"0fe10511111111", 7, 3}, + {"0fe20511111111", 7, 3}, + {"0fe30511111111", 7, 3}, + {"0fe40511111111", 7, 3}, + {"0fe50511111111", 7, 3}, + {"660fe60511111111", 8, 4}, + {"0fe70511111111", 7, 3}, + {"0fe80511111111", 7, 3}, + {"0fe90511111111", 7, 3}, + {"0fea0511111111", 7, 3}, + {"0feb0511111111", 7, 3}, + {"0fec0511111111", 7, 3}, + {"0fed0511111111", 7, 3}, + {"0fee0511111111", 7, 3}, + {"0fef0511111111", 7, 3}, + {"f20ff00511111111", 8, 4}, + {"0ff10511111111", 7, 3}, + {"0ff20511111111", 7, 3}, + {"0ff30511111111", 7, 3}, + {"0ff40511111111", 7, 3}, + {"0ff50511111111", 7, 3}, + {"0ff60511111111", 7, 3}, + {"0ff80511111111", 7, 3}, + {"0ff90511111111", 7, 3}, + {"0ffa0511111111", 7, 3}, + {"0ffb0511111111", 7, 3}, + {"0ffc0511111111", 7, 3}, + {"0ffd0511111111", 7, 3}, + {"0ffe0511111111", 7, 3}, + {"0f38000511111111", 8, 4}, + {"0f38010511111111", 8, 4}, + {"0f38020511111111", 8, 4}, + {"0f38030511111111", 8, 4}, + {"0f38040511111111", 8, 4}, + {"0f38050511111111", 8, 4}, + {"0f38060511111111", 8, 4}, + {"0f38070511111111", 8, 4}, + {"0f38080511111111", 8, 4}, + {"0f38090511111111", 8, 4}, + {"0f380a0511111111", 8, 4}, + {"0f380b0511111111", 8, 4}, + {"660f38100511111111", 9, 5}, + {"660f38140511111111", 9, 5}, + {"660f38150511111111", 9, 5}, + {"660f38170511111111", 9, 5}, + {"0f381c0511111111", 8, 4}, + {"0f381d0511111111", 8, 4}, + {"0f381e0511111111", 8, 4}, + {"660f38200511111111", 9, 5}, + {"660f38210511111111", 9, 5}, + {"660f38220511111111", 9, 5}, + {"660f38230511111111", 9, 5}, + {"660f38240511111111", 9, 5}, + {"660f38250511111111", 9, 5}, + {"660f38280511111111", 9, 5}, + {"660f38290511111111", 9, 5}, + {"660f382a0511111111", 9, 5}, + {"660f382b0511111111", 9, 5}, + {"660f38300511111111", 9, 5}, + {"660f38310511111111", 9, 5}, + {"660f38320511111111", 9, 5}, + {"660f38330511111111", 9, 5}, + {"660f38340511111111", 9, 5}, + {"660f38350511111111", 9, 5}, + {"660f38370511111111", 9, 5}, + {"660f38380511111111", 9, 5}, + {"660f38390511111111", 9, 5}, + {"660f383a0511111111", 9, 5}, + {"660f383b0511111111", 9, 5}, + {"660f383c0511111111", 9, 5}, + {"660f383d0511111111", 9, 5}, + {"660f383e0511111111", 9, 5}, + {"660f383f0511111111", 9, 5}, + {"660f38400511111111", 9, 5}, + {"660f38410511111111", 9, 5}, + {"660f38820511111111", 9, 5}, + {"0f38c80511111111", 8, 4}, + {"0f38c90511111111", 8, 4}, + {"0f38ca0511111111", 8, 4}, + {"0f38cb0511111111", 8, 4}, + {"0f38cc0511111111", 8, 4}, + {"0f38cd0511111111", 8, 4}, + {"f30f38d80511111111", 9, 5}, + {"660f38db0511111111", 9, 5}, + {"660f38dc0511111111", 9, 5}, + {"660f38dd0511111111", 9, 5}, + {"660f38de0511111111", 9, 5}, + {"660f38df0511111111", 9, 5}, + {"0f38f00511111111", 8, 4}, + {"0f38f10511111111", 8, 4}, + {"660f38f60511111111", 9, 5}, + {"660f3a08051111111111", 10, 5}, + {"660f3a09051111111111", 10, 5}, + {"660f3a0a051111111111", 10, 5}, + {"660f3a0b051111111111", 10, 5}, + {"660f3a0c051111111111", 10, 5}, + {"660f3a0d051111111111", 10, 5}, + {"660f3a0e051111111111", 10, 5}, + {"0f3a0f051111111111", 9, 4}, + {"660f3a14051111111111", 10, 5}, + {"660f3a15051111111111", 10, 5}, + {"660f3a16051111111111", 10, 5}, + {"660f3a17051111111111", 10, 5}, + {"660f3a20051111111111", 10, 5}, + {"660f3a21051111111111", 10, 5}, + {"660f3a22051111111111", 10, 5}, + {"660f3a40051111111111", 10, 5}, + {"660f3a41051111111111", 10, 5}, + {"660f3a42051111111111", 10, 5}, + {"660f3a44051111111111", 10, 5}, + {"660f3a60051111111111", 10, 5}, + {"660f3a61051111111111", 10, 5}, + {"660f3a62051111111111", 10, 5}, + {"660f3a63051111111111", 10, 5}, + {"0f3acc051111111111", 9, 4}, + {"660f3adf051111111111", 10, 5}, + {"c5f8100511111111", 8, 4}, + {"c5f8110511111111", 8, 4}, + {"c5f8120511111111", 8, 4}, + {"c5f8130511111111", 8, 4}, + {"c5f8140511111111", 8, 4}, + {"c5f8150511111111", 8, 4}, + {"c5f8160511111111", 8, 4}, + {"c5f8170511111111", 8, 4}, + {"c5f8280511111111", 8, 4}, + {"c5f8290511111111", 8, 4}, + {"c5fa2a0511111111", 8, 4}, + {"c5f82b0511111111", 8, 4}, + {"c5fa2c0511111111", 8, 4}, + {"c5fa2d0511111111", 8, 4}, + {"c5f82e0511111111", 8, 4}, + {"c5f82f0511111111", 8, 4}, + {"c5f8510511111111", 8, 4}, + {"c5f8520511111111", 8, 4}, + {"c5f8530511111111", 8, 4}, + {"c5f8540511111111", 8, 4}, + {"c5f8550511111111", 8, 4}, + {"c5f8560511111111", 8, 4}, + {"c5f8570511111111", 8, 4}, + {"c5f8580511111111", 8, 4}, + {"c5f8590511111111", 8, 4}, + {"c5f85a0511111111", 8, 4}, + {"c5f85b0511111111", 8, 4}, + {"c5f85c0511111111", 8, 4}, + {"c5f85d0511111111", 8, 4}, + {"c5f85e0511111111", 8, 4}, + {"c5f85f0511111111", 8, 4}, + {"c5f9600511111111", 8, 4}, + {"c5f9610511111111", 8, 4}, + {"c5f9620511111111", 8, 4}, + {"c5f9630511111111", 8, 4}, + {"c5f9640511111111", 8, 4}, + {"c5f9650511111111", 8, 4}, + {"c5f9660511111111", 8, 4}, + {"c5f9670511111111", 8, 4}, + {"c5f9680511111111", 8, 4}, + {"c5f9690511111111", 8, 4}, + {"c5f96a0511111111", 8, 4}, + {"c5f96b0511111111", 8, 4}, + {"c5f96c0511111111", 8, 4}, + {"c5f96d0511111111", 8, 4}, + {"c5f96e0511111111", 8, 4}, + {"c5f96f0511111111", 8, 4}, + {"c5f970051111111111", 9, 4}, + {"c5f9740511111111", 8, 4}, + {"c5f9750511111111", 8, 4}, + {"c5f9760511111111", 8, 4}, + {"c5f97c0511111111", 8, 4}, + {"c5f97d0511111111", 8, 4}, + {"c5f97e0511111111", 8, 4}, + {"c5f97f0511111111", 8, 4}, + {"c5f8ae1511111111", 8, 4}, + {"c5f8c2051111111111", 9, 4}, + {"c5f9c4051111111111", 9, 4}, + {"c5f8c6051111111111", 9, 4}, + {"c5f9d00511111111", 8, 4}, + {"c5f9d10511111111", 8, 4}, + {"c5f9d20511111111", 8, 4}, + {"c5f9d30511111111", 8, 4}, + {"c5f9d40511111111", 8, 4}, + {"c5f9d50511111111", 8, 4}, + {"c5f9d60511111111", 8, 4}, + {"c5f9d80511111111", 8, 4}, + {"c5f9d90511111111", 8, 4}, + {"c5f9da0511111111", 8, 4}, + {"c5f9db0511111111", 8, 4}, + {"c5f9dc0511111111", 8, 4}, + {"c5f9dd0511111111", 8, 4}, + {"c5f9de0511111111", 8, 4}, + {"c5f9df0511111111", 8, 4}, + {"c5f9e00511111111", 8, 4}, + {"c5f9e10511111111", 8, 4}, + {"c5f9e20511111111", 8, 4}, + {"c5f9e30511111111", 8, 4}, + {"c5f9e40511111111", 8, 4}, + {"c5f9e50511111111", 8, 4}, + {"c5f9e60511111111", 8, 4}, + {"c5f9e70511111111", 8, 4}, + {"c5f9e80511111111", 8, 4}, + {"c5f9e90511111111", 8, 4}, + {"c5f9ea0511111111", 8, 4}, + {"c5f9eb0511111111", 8, 4}, + {"c5f9ec0511111111", 8, 4}, + {"c5f9ed0511111111", 8, 4}, + {"c5f9ee0511111111", 8, 4}, + {"c5f9ef0511111111", 8, 4}, + {"c5fbf00511111111", 8, 4}, + {"c5f9f10511111111", 8, 4}, + {"c5f9f20511111111", 8, 4}, + {"c5f9f30511111111", 8, 4}, + {"c5f9f40511111111", 8, 4}, + {"c5f9f50511111111", 8, 4}, + {"c5f9f60511111111", 8, 4}, + {"c5f9f80511111111", 8, 4}, + {"c5f9f90511111111", 8, 4}, + {"c5f9fa0511111111", 8, 4}, + {"c5f9fb0511111111", 8, 4}, + {"c5f9fc0511111111", 8, 4}, + {"c5f9fd0511111111", 8, 4}, + {"c5f9fe0511111111", 8, 4}, + {"c4e178100511111111", 9, 5}, + {"c4e178110511111111", 9, 5}, + {"c4e178120511111111", 9, 5}, + {"c4e178130511111111", 9, 5}, + {"c4e178140511111111", 9, 5}, + {"c4e178150511111111", 9, 5}, + {"c4e178160511111111", 9, 5}, + {"c4e178170511111111", 9, 5}, + {"c4e178280511111111", 9, 5}, + {"c4e178290511111111", 9, 5}, + {"c4e17a2a0511111111", 9, 5}, + {"c4e1782b0511111111", 9, 5}, + {"c4e17a2c0511111111", 9, 5}, + {"c4e17a2d0511111111", 9, 5}, + {"c4e1782e0511111111", 9, 5}, + {"c4e1782f0511111111", 9, 5}, + {"c4e178510511111111", 9, 5}, + {"c4e178520511111111", 9, 5}, + {"c4e178530511111111", 9, 5}, + {"c4e178540511111111", 9, 5}, + {"c4e178550511111111", 9, 5}, + {"c4e178560511111111", 9, 5}, + {"c4e178570511111111", 9, 5}, + {"c4e178580511111111", 9, 5}, + {"c4e178590511111111", 9, 5}, + {"c4e1785a0511111111", 9, 5}, + {"c4e1785b0511111111", 9, 5}, + {"c4e1785c0511111111", 9, 5}, + {"c4e1785d0511111111", 9, 5}, + {"c4e1785e0511111111", 9, 5}, + {"c4e1785f0511111111", 9, 5}, + {"c4e179600511111111", 9, 5}, + {"c4e179610511111111", 9, 5}, + {"c4e179620511111111", 9, 5}, + {"c4e179630511111111", 9, 5}, + {"c4e179640511111111", 9, 5}, + {"c4e179650511111111", 9, 5}, + {"c4e179660511111111", 9, 5}, + {"c4e179670511111111", 9, 5}, + {"c4e179680511111111", 9, 5}, + {"c4e179690511111111", 9, 5}, + {"c4e1796a0511111111", 9, 5}, + {"c4e1796b0511111111", 9, 5}, + {"c4e1796c0511111111", 9, 5}, + {"c4e1796d0511111111", 9, 5}, + {"c4e1796e0511111111", 9, 5}, + {"c4e1796f0511111111", 9, 5}, + {"c4e17970051111111111", 10, 5}, + {"c4e179740511111111", 9, 5}, + {"c4e179750511111111", 9, 5}, + {"c4e179760511111111", 9, 5}, + {"c4e1797c0511111111", 9, 5}, + {"c4e1797d0511111111", 9, 5}, + {"c4e1797e0511111111", 9, 5}, + {"c4e1797f0511111111", 9, 5}, + {"c4e178ae1511111111", 9, 5}, + {"c4e178c2051111111111", 10, 5}, + {"c4e179c4051111111111", 10, 5}, + {"c4e178c6051111111111", 10, 5}, + {"c4e179d00511111111", 9, 5}, + {"c4e179d10511111111", 9, 5}, + {"c4e179d20511111111", 9, 5}, + {"c4e179d30511111111", 9, 5}, + {"c4e179d40511111111", 9, 5}, + {"c4e179d50511111111", 9, 5}, + {"c4e179d60511111111", 9, 5}, + {"c4e179d80511111111", 9, 5}, + {"c4e179d90511111111", 9, 5}, + {"c4e179da0511111111", 9, 5}, + {"c4e179db0511111111", 9, 5}, + {"c4e179dc0511111111", 9, 5}, + {"c4e179dd0511111111", 9, 5}, + {"c4e179de0511111111", 9, 5}, + {"c4e179df0511111111", 9, 5}, + {"c4e179e00511111111", 9, 5}, + {"c4e179e10511111111", 9, 5}, + {"c4e179e20511111111", 9, 5}, + {"c4e179e30511111111", 9, 5}, + {"c4e179e40511111111", 9, 5}, + {"c4e179e50511111111", 9, 5}, + {"c4e179e60511111111", 9, 5}, + {"c4e179e70511111111", 9, 5}, + {"c4e179e80511111111", 9, 5}, + {"c4e179e90511111111", 9, 5}, + {"c4e179ea0511111111", 9, 5}, + {"c4e179eb0511111111", 9, 5}, + {"c4e179ec0511111111", 9, 5}, + {"c4e179ed0511111111", 9, 5}, + {"c4e179ee0511111111", 9, 5}, + {"c4e179ef0511111111", 9, 5}, + {"c4e17bf00511111111", 9, 5}, + {"c4e179f10511111111", 9, 5}, + {"c4e179f20511111111", 9, 5}, + {"c4e179f30511111111", 9, 5}, + {"c4e179f40511111111", 9, 5}, + {"c4e179f50511111111", 9, 5}, + {"c4e179f60511111111", 9, 5}, + {"c4e179f80511111111", 9, 5}, + {"c4e179f90511111111", 9, 5}, + {"c4e179fa0511111111", 9, 5}, + {"c4e179fb0511111111", 9, 5}, + {"c4e179fc0511111111", 9, 5}, + {"c4e179fd0511111111", 9, 5}, + {"c4e179fe0511111111", 9, 5}, + {"c4e279000511111111", 9, 5}, + {"c4e279010511111111", 9, 5}, + {"c4e279020511111111", 9, 5}, + {"c4e279030511111111", 9, 5}, + {"c4e279040511111111", 9, 5}, + {"c4e279050511111111", 9, 5}, + {"c4e279060511111111", 9, 5}, + {"c4e279070511111111", 9, 5}, + {"c4e279080511111111", 9, 5}, + {"c4e279090511111111", 9, 5}, + {"c4e2790a0511111111", 9, 5}, + {"c4e2790b0511111111", 9, 5}, + {"c4e2790c0511111111", 9, 5}, + {"c4e2790d0511111111", 9, 5}, + {"c4e2790e0511111111", 9, 5}, + {"c4e2790f0511111111", 9, 5}, + {"c4e279130511111111", 9, 5}, + {"c4e27d160511111111", 9, 5}, + {"c4e279170511111111", 9, 5}, + {"c4e279180511111111", 9, 5}, + {"c4e27d190511111111", 9, 5}, + {"c4e27d1a0511111111", 9, 5}, + {"c4e2791c0511111111", 9, 5}, + {"c4e2791d0511111111", 9, 5}, + {"c4e2791e0511111111", 9, 5}, + {"c4e279200511111111", 9, 5}, + {"c4e279210511111111", 9, 5}, + {"c4e279220511111111", 9, 5}, + {"c4e279230511111111", 9, 5}, + {"c4e279240511111111", 9, 5}, + {"c4e279250511111111", 9, 5}, + {"c4e279280511111111", 9, 5}, + {"c4e279290511111111", 9, 5}, + {"c4e2792a0511111111", 9, 5}, + {"c4e2792b0511111111", 9, 5}, + {"c4e2792c0511111111", 9, 5}, + {"c4e2792d0511111111", 9, 5}, + {"c4e2792e0511111111", 9, 5}, + {"c4e2792f0511111111", 9, 5}, + {"c4e279300511111111", 9, 5}, + {"c4e279310511111111", 9, 5}, + {"c4e279320511111111", 9, 5}, + {"c4e279330511111111", 9, 5}, + {"c4e279340511111111", 9, 5}, + {"c4e279350511111111", 9, 5}, + {"c4e27d360511111111", 9, 5}, + {"c4e279370511111111", 9, 5}, + {"c4e279380511111111", 9, 5}, + {"c4e279390511111111", 9, 5}, + {"c4e2793a0511111111", 9, 5}, + {"c4e2793b0511111111", 9, 5}, + {"c4e2793c0511111111", 9, 5}, + {"c4e2793d0511111111", 9, 5}, + {"c4e2793e0511111111", 9, 5}, + {"c4e2793f0511111111", 9, 5}, + {"c4e279400511111111", 9, 5}, + {"c4e279410511111111", 9, 5}, + {"c4e279450511111111", 9, 5}, + {"c4e279460511111111", 9, 5}, + {"c4e279470511111111", 9, 5}, + {"c4e279580511111111", 9, 5}, + {"c4e279590511111111", 9, 5}, + {"c4e27d5a0511111111", 9, 5}, + {"c4e279780511111111", 9, 5}, + {"c4e279790511111111", 9, 5}, + {"c4e2798c0511111111", 9, 5}, + {"c4e2798e0511111111", 9, 5}, + {"c4e279960511111111", 9, 5}, + {"c4e279970511111111", 9, 5}, + {"c4e279980511111111", 9, 5}, + {"c4e279990511111111", 9, 5}, + {"c4e2799a0511111111", 9, 5}, + {"c4e2799b0511111111", 9, 5}, + {"c4e2799c0511111111", 9, 5}, + {"c4e2799d0511111111", 9, 5}, + {"c4e2799e0511111111", 9, 5}, + {"c4e2799f0511111111", 9, 5}, + {"c4e279a60511111111", 9, 5}, + {"c4e279a70511111111", 9, 5}, + {"c4e279a80511111111", 9, 5}, + {"c4e279a90511111111", 9, 5}, + {"c4e279aa0511111111", 9, 5}, + {"c4e279ab0511111111", 9, 5}, + {"c4e279ac0511111111", 9, 5}, + {"c4e279ad0511111111", 9, 5}, + {"c4e279ae0511111111", 9, 5}, + {"c4e279af0511111111", 9, 5}, + {"c4e279b60511111111", 9, 5}, + {"c4e279b70511111111", 9, 5}, + {"c4e279b80511111111", 9, 5}, + {"c4e279b90511111111", 9, 5}, + {"c4e279ba0511111111", 9, 5}, + {"c4e279bb0511111111", 9, 5}, + {"c4e279bc0511111111", 9, 5}, + {"c4e279bd0511111111", 9, 5}, + {"c4e279be0511111111", 9, 5}, + {"c4e279bf0511111111", 9, 5}, + {"c4e279db0511111111", 9, 5}, + {"c4e279dc0511111111", 9, 5}, + {"c4e279dd0511111111", 9, 5}, + {"c4e279de0511111111", 9, 5}, + {"c4e279df0511111111", 9, 5}, + {"c4e278f20511111111", 9, 5}, + {"c4e278f30d11111111", 9, 5}, + {"c4e278f50511111111", 9, 5}, + {"c4e27bf60511111111", 9, 5}, + {"c4e278f70511111111", 9, 5}, + {"c4e3fd00051111111111", 10, 5}, + {"c4e3fd01051111111111", 10, 5}, + {"c4e37902051111111111", 10, 5}, + {"c4e37904051111111111", 10, 5}, + {"c4e37905051111111111", 10, 5}, + {"c4e37d06051111111111", 10, 5}, + {"c4e37908051111111111", 10, 5}, + {"c4e37909051111111111", 10, 5}, + {"c4e3790a051111111111", 10, 5}, + {"c4e3790b051111111111", 10, 5}, + {"c4e3790c051111111111", 10, 5}, + {"c4e3790d051111111111", 10, 5}, + {"c4e3790e051111111111", 10, 5}, + {"c4e3790f051111111111", 10, 5}, + {"c4e37914051111111111", 10, 5}, + {"c4e37915051111111111", 10, 5}, + {"c4e37916051111111111", 10, 5}, + {"c4e37917051111111111", 10, 5}, + {"c4e37d18051111111111", 10, 5}, + {"c4e37d19051111111111", 10, 5}, + {"c4e3791d051111111111", 10, 5}, + {"c4e37920051111111111", 10, 5}, + {"c4e37921051111111111", 10, 5}, + {"c4e37922051111111111", 10, 5}, + {"c4e37d38051111111111", 10, 5}, + {"c4e37d39051111111111", 10, 5}, + {"c4e37940051111111111", 10, 5}, + {"c4e37941051111111111", 10, 5}, + {"c4e37942051111111111", 10, 5}, + {"c4e37944051111111111", 10, 5}, + {"c4e37d46051111111111", 10, 5}, + {"c4e3794a051111111111", 10, 5}, + {"c4e3794b051111111111", 10, 5}, + {"c4e3794c051111111111", 10, 5}, + {"c4e37960051111111111", 10, 5}, + {"c4e37961051111111111", 10, 5}, + {"c4e37962051111111111", 10, 5}, + {"c4e37963051111111111", 10, 5}, + {"c4e379df051111111111", 10, 5}, + {"c4e37bf0051111111111", 10, 5}, +}; + +inline constexpr DecoderBranchCase kDecoderBranchCases[] = { + {"0f0b", 2, Codegen::ControlFlowKind::Trap, false, 0}, + {"0f80c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f81c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f82c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f83c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f84c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f85c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f86c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f87c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f88c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f89c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8ac0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8bc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8cc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8dc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8ec0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8fc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0fb9c0", 3, Codegen::ControlFlowKind::Trap, false, 0}, + {"480fb9c0", 4, Codegen::ControlFlowKind::Trap, false, 0}, + {"48cac011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"48cb", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"660fb9c0", 4, Codegen::ControlFlowKind::Trap, false, 0}, + {"66c2c011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"66c3", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"66cac011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"66cb", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"66ff18", 3, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"66ff28", 3, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"66ffd0", 3, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"66ffe0", 3, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"70c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"71c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"72c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"73c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"74c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"75c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"76c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"77c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"78c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"79c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7ac0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7bc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7cc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7dc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7ec0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7fc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"c2c011", 3, Codegen::ControlFlowKind::Return, false, 0}, + {"c3", 1, Codegen::ControlFlowKind::Return, false, 0}, + {"cac011", 3, Codegen::ControlFlowKind::Return, false, 0}, + {"cb", 1, Codegen::ControlFlowKind::Return, false, 0}, + {"e0c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e1c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e2c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e3c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e8c0111111", 5, Codegen::ControlFlowKind::Call, true, 286331328}, + {"e9c0111111", 5, Codegen::ControlFlowKind::UnconditionalJump, true, 286331328}, + {"ebc0", 2, Codegen::ControlFlowKind::UnconditionalJump, true, -64}, + {"ff1511111111", 6, Codegen::ControlFlowKind::Call, false, 0}, + {"ff1d11111111", 6, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ff2511111111", 6, Codegen::ControlFlowKind::UnconditionalJump, true, 286331153}, + {"ff2d11111111", 6, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"ff18", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ff28", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"ffd0", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ffe0", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, +}; + +#endif diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 76f460b608..ca63d7f56a 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -336,7 +336,7 @@ std::vector LinuxElfPatcher::Patch( const std::uint16_t writtenPh = _programHeaderLayoutBuilder->WriteLayout(buf, layoutRequest); _byteWriter->WriteU16(buf, kEhdrPhNumOffset, writtenPh); if (_fixedBase) - RebaseProgramHeaders(buf, phOff, phEntSize, writtenPh, base); + RebaseProgramHeaders(buf, tableOff, phEntSize, writtenPh, base); SectionHeaderTableRequest sectionRequest{}; sectionRequest.DynStrOffset = dynStrOff; diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 76598e35ae..1fb61430c6 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -55,7 +55,6 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma Io::WriteU32(symbols, index * 24, addString("__tls_get_addr")); needsTlsResolver = true; } - if (symbol.Section == 0 && (symbol.Info >> 4) == 2) symbols[index * 24 + 4] = static_cast(0x10 | (symbol.Info & 15)); } std::vector needed; for (const auto& dependency : dependencies) needed.push_back(addString(dependency)); @@ -141,18 +140,18 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma bytes.insert(bytes.end(), stub.begin(), stub.end()); return start; }; - const auto callThrough = [&](std::uint64_t target) { + const auto finalizer = [&](std::uint64_t target) { if (target == 0) return std::uint64_t{}; const auto start = address(); - const auto distance = static_cast(target) - static_cast(start + 5); + const auto distance = static_cast(target) - static_cast(start + 11); if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) throw Domain::RelinkerException("Guest finaliser exceeds relative branch range"); - bytes.push_back(0xe9); + bytes.insert(bytes.end(), {0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xe9}); Io::AppendU32(bytes, static_cast(distance)); return start; }; const auto init = lifecycle(image.Init); - const auto fini = callThrough(image.Fini); + const auto fini = finalizer(image.Fini); Io::AlignBuffer(bytes, 8); const auto dynamicOffset = bytes.size(); const auto dynamicAddress = address(); diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index bd5928a33a..74e31d19d3 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -83,7 +83,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); const auto name = guest.UsePlatformTlsResolver && symbol.Name == "vNe1w4diLCs" ? std::string("Aps5GuestTlsGetAddr_nid_no_patch") : DynamicName(guest, symbolIndex); - input.Binds.push_back({"_" + name, rva, static_cast(addend)}); + input.Binds.push_back({"_" + name, rva, static_cast(addend), (symbol.Info >> 4) == 2}); } } else throw Domain::RelinkerException("Unsupported macOS guest relocation " + std::to_string(type), target); } diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 0b36789a41..f15b546181 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -41,7 +41,6 @@ std::vector ReadNeededLibraries(const Domain::SysVDynamicSection& d if (name.empty()) throw Domain::RelinkerException("Invalid DT_NEEDED library: " + name); if (unique.insert(name).second) result.push_back(std::move(name)); } - if (unique.contains("libSceLibcInternal.prx") && !unique.contains("libc.prx")) result.push_back("libc.prx"); return result; } @@ -71,8 +70,9 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend), import.Weak}); for (const auto& library : ReadNeededLibraries(dynamicSection)) input.Dylibs.push_back(MachODependency(library, "@executable_path")); - if (std::find(input.Dylibs.begin(), input.Dylibs.end(), "@rpath/libkernel.prx") == input.Dylibs.end()) - input.Dylibs.push_back("@rpath/libkernel.prx"); + for (const auto* library : {"@rpath/libkernel.prx", "@rpath/libc.prx"}) + if (std::find(input.Dylibs.begin(), input.Dylibs.end(), library) == input.Dylibs.end()) + input.Dylibs.push_back(library); input.RunPaths = MachORunPaths(runPath, "@executable_path"); input.EntryRva = image.GetEntryRva(); input.Trampolines = &trampolines; diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 3901ef1c17..ed4f72c88e 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -52,7 +52,7 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD // AnyPS5 implements the C runtime in libc.prx. Windows GetProcAddress // does not search a module's dependencies as ELF symbol lookup does. // Keep the requested module first, then make its shared runtime visible. - if (unique.contains("libSceLibcInternal.prx") && !unique.contains("libc.prx")) + if ((unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) && !unique.contains("libc.prx")) result.push_back("libc.prx"); return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2a017013c4..e50f17c0ce 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -99,7 +99,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co std::vector accesses; std::set branchTargets; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }); + std::set entryRoots; + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots); std::uint64_t scanned = 0; for (const auto& header : headers) { @@ -120,6 +121,11 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const auto rva = image.GetRva(header.MappedAddress + offset, info.Length); const bool insidePrevious = *instruction < decodedEnd; decodedEnd = std::max(decodedEnd, *instruction + info.Length); + const auto checkProtectedEntry = [&](const std::size_t length) { + const auto entry = entryRoots.upper_bound(*instruction); + if (entry != entryRoots.end() && *entry - *instruction < length) + throw Domain::RelinkerException("TLS rewrite would overwrite an executable entry point", header.Offset + *entry - header.MappedAddress); + }; if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = static_cast(rva) + static_cast(info.Length) + info.BranchDisp; @@ -143,7 +149,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const bool wide = (info.RexPrefix & 8) != 0; const bool isAlu = isAluReadOpcode(bytes[position]); const auto address = supportedPrefixes && !loadValue && !aluRead && (bytes[position] == 0x8b || isAlu) && (wide || !hasOperandSizePrefix) ? registerAddress(bytes, position, info.Length, info.RexPrefix) : std::nullopt; - const auto addressRegister = static_cast(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)); + const auto addressRegister = address ? static_cast(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)) : std::uint8_t{4}; if (address && addressRegister != 4) { std::vector moved; auto length = info.Length; @@ -175,6 +181,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } + checkProtectedEntry(length); accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)}); continue; } @@ -186,6 +193,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co message << ')'; throw Domain::RelinkerException(message.str(), header.Offset + offset); } + checkProtectedEntry(info.Length); accesses.push_back({rva, header.Offset + offset, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister, storeImmediate ? 0 : Io::ReadU32(source, header.Offset + offset + position + 3), aluRead ? bytes[position] : std::uint8_t{0}}); } } diff --git a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp index 708d39dc7b..b99a198578 100644 --- a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp +++ b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp @@ -45,6 +45,13 @@ extern "C" void SetPendingArgs(std::size_t args, const void* argp) { pending.argp = argp; } +extern "C" void APS5_VABI RecordFini(std::size_t args, const void* argp, const void* callback) { + char line[112]; + std::snprintf(line, sizeof(line), "fini=%016zx argp=%016zx callback=%016zx\n", args, + reinterpret_cast(argp), reinterpret_cast(callback)); + Write(line); +} + extern "C" const void* __aps5_get_pending_module_args_nid_no_patch() { return &pending; } diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index d23e355840..2635ae20e5 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -46,13 +46,14 @@ void checkRuntimeDependencies() { throw std::runtime_error("Incorrect Windows runtime dependency visibility"); }; check({}, {}); - check({"libkernel.prx"}, {"libkernel.prx"}); + check({"libkernel.prx"}, {"libkernel.prx", "libc.prx"}); + check({"libkernel.prx", "libc.prx"}, {"libkernel.prx", "libc.prx"}); check({"libSceLibcInternal.prx", "libkernel.prx"}, {"libSceLibcInternal.prx", "libkernel.prx", "libc.prx"}); check({"libc.prx", "libSceLibcInternal.prx"}, {"libc.prx", "libSceLibcInternal.prx"}); check({"libSceVideoOut.prx", "libkernel.prx", "libSceVideoOut.prx"}, - {"libSceVideoOut.prx", "libkernel.prx"}); + {"libSceVideoOut.prx", "libkernel.prx", "libc.prx"}); check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 900518b16f..8ec3b79abd 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -51,7 +51,7 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa const auto output = directory / (leftover ? "leftover" : "running"); if (leftover) std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; - std::filesystem::copy_file("/bin/sleep", output); + std::filesystem::copy_file(std::filesystem::read_symlink("/proc/self/exe"), output); const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_all; std::filesystem::permissions(output, permissions); const auto previous = readFile(output); @@ -59,7 +59,7 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa if (child < 0) throw std::runtime_error("Cannot start the previous executable"); if (child == 0) { - execl(output.c_str(), "sleep", "30", nullptr); + execl(output.c_str(), output.c_str(), "wait", nullptr); _exit(127); } std::error_code error; @@ -172,6 +172,10 @@ int main(int argc, char* argv[]) { try { if (argc != 2) throw std::runtime_error("Expected binary or text"); const std::string mode = argv[1]; + if (mode == "wait") { + sleep(30); + return 0; + } for (const std::size_t size : {std::size_t{3}, std::size_t{65536}}) { if (mode == "binary") { const std::vector data(size, 0x41); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 6d486d6112..325b6f3399 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -210,7 +210,7 @@ int Run(const int argc, char* argv[]) { 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()) + if (std::any_of(moduleDirectories.begin(), moduleDirectories.end(), [&](const auto& directory) { return Cli::SameDirectory(directory, input.parent_path()); })) 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); diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index 9169186da7..de93097012 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -9,7 +9,7 @@ namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr) const; }; } diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73..8b83ab475e 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace Relinker { @@ -35,7 +36,7 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -438,6 +439,7 @@ std::set CodeInstructionCollector::Collect(const std::ve } } report(10000); + if (entryRoots) *entryRoots = std::move(roots); return instructions; } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index fd61157b6a..35e6193473 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -126,13 +126,22 @@ private: default: throw RelinkerException("Strict filter: unsupported unwind pointer format", field); } if (value == 0) return 0; + VirtualAddress base = 0; switch (encoding & 0x70) { case 0: break; - case 0x10: value += field; break; - case 0x30: value += dataBase; break; - case 0x40: value += functionBase; break; + case 0x10: base = field; break; + case 0x30: base = dataBase; break; + case 0x40: base = functionBase; break; default: throw RelinkerException("Strict filter: unsupported unwind pointer base", field); } + if ((encoding & 0x70) != 0) { + if ((encoding & 0x08) != 0) { + const bool negative = (value >> 63) != 0; + if (negative ? base < std::uint64_t{0} - value : value > std::numeric_limits::max() - base) + throw RelinkerException("Strict filter: unwind pointer address overflowed", field); + } + value = base + value; + } if ((encoding & 0x80) != 0 && !importSlots.contains(value)) { const auto pointer = pointers.find(value); if (pointer != pointers.end()) value = pointer->second; @@ -198,7 +207,7 @@ private: const auto end = checkedRangeEnd(position, length); const auto cieField = position; const auto cieOffset = read(position); - if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); + if (cieOffset == 0 || cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); const auto cie = readCie(cieField - cieOffset); const auto begin = encoded(position, cie.PointerEncoding); const auto size = encoded(position, cie.PointerEncoding & 0x0F); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 8e082aa3e9..cf7f7a80ae 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -19,7 +19,9 @@ using namespace Elfpatcher; namespace { +constexpr std::uint16_t kEtNone = 0; constexpr std::uint16_t kEtRel = 1; +constexpr std::uint16_t kEtCore = 4; } @@ -55,8 +57,13 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { } RelinkResult RelinkerPipeline::Relink() { - if (elfReader->ReadHeader().Type == kEtRel) + const auto inputType = elfReader->ReadHeader().Type; + if (inputType == kEtRel) throw RelinkerException("ET_REL relocatable inputs are not supported"); + if (inputType == kEtNone) + throw RelinkerException("ET_NONE inputs do not specify an ELF object type"); + if (inputType == kEtCore) + throw RelinkerException("ET_CORE core dumps are not executable images"); auto programHeaders = elfReader->ReadProgramHeaders(); diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index 0210073af7..a0b8529573 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -26,7 +26,15 @@ int main() { auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; auto first = load; first.FileSize = 72; Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); - for (int kind = 0; kind < 7; ++kind) { + auto belowBytes = Fixture(); + belowBytes[3] = 0x30; + Io::WriteU64(belowBytes, 16, 0x800 - 0x1000); + Io::WriteU64(belowBytes, 24, 0x1060 - 0x1000); + Io::WriteU64(belowBytes, 104, 0x800); + Domain::ProgramHeader funcSeg{}; funcSeg.Type = 1; funcSeg.MappedAddress = 0x800; funcSeg.FileSize = 0x20; + const auto below = Relinker::UnusedNidFilter::ReadExceptionFunctions(belowBytes, {funcSeg, load, eh}, {}, {}); + Require(below.size() == 1 && below[0].Begin == 0x800 && below[0].End == 0x810); + for (int kind = 0; kind < 10; ++kind) { auto bytes = Fixture(); auto segment = load; switch (kind) { case 0: Io::WriteU32(bytes, 64, 0x1000); break; @@ -36,12 +44,26 @@ int main() { case 4: segment.Offset = std::numeric_limits::max() - 4095; segment.MappedAddress = 0; segment.FileSize = 0x1100; break; case 5: segment.FileSize = 119; break; case 6: Io::WriteU32(bytes, 64, 8); break; + case 7: Io::WriteU32(bytes, 100, 0); break; + case 8: { + bytes[3] = 0x1B; + Io::WriteU32(bytes, 16, 0xFFFFE000); + Io::WriteU32(bytes, 20, 0x1060 - 0x1014); + Io::WriteU64(bytes, 104, 0xFFFFFFFFFFFFF010ULL); + break; + } + case 9: bytes[3] = 0x1C; Io::WriteU64(bytes, 16, 0x8000000000000000ULL); break; } bool rejected = false; - try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } + std::vector headers = {segment, eh}; + if (kind == 8) { + Domain::ProgramHeader high{}; high.Type = 1; high.MappedAddress = 0xFFFFFFFFFFFFF000ULL; high.FileSize = 0x100; + headers.push_back(high); + } + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); } catch (const std::exception&) { rejected = true; } Require(rejected); } - std::cout << "9 unwind record cases passed\n"; + std::cout << "13 unwind record cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/relinker/tests/macos/import_fixture.py b/core/relinker/relinker/tests/macos/import_fixture.py index 5e3c0a4914..7746eda98c 100644 --- a/core/relinker/relinker/tests/macos/import_fixture.py +++ b/core/relinker/relinker/tests/macos/import_fixture.py @@ -1,9 +1,9 @@ -"""A minimal PS5-style ELF that imports puts and exit from libc.prx by NID.""" +"""A minimal PS5-style ELF that imports puts and exit by NID from one needed library, libc.prx by default.""" import struct, sys sys.path.insert(0, __import__("os").path.dirname(__import__("os").path.abspath(__file__))) from nid import nid -def fixture(message=b"hello from a PS5 ELF on macOS\0"): +def fixture(message=b"hello from a PS5 ELF on macOS\0", library=b"libc.prx"): image = bytearray(0x6000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into(" struct.unpack_from(" second -> first; entry/getargv arguments, Unicode, cwd/resources, ' - 'rejected targets, shutdown without guest or host exit handlers passed') + directory = tempfile.mkdtemp(prefix='anyps5-loadexec-') + try: + work = Path(directory) / 'Документы 東京' + title = work / 'title' + output = work / 'native output' + (title / 'picker').mkdir(parents=True) + output.mkdir() + (title / 'sce_module').mkdir() + (title / 'eboot.bin').write_bytes(fixture('LoadExecFirst_nid_no_patch')) + (title / 'picker' / 'second.bin').write_bytes(fixture('LoadExecSecond_nid_no_patch')) + (title / 'resource.txt').write_text('resource survives\n') + app = output / ('app.exe' if windows else 'app.elf') + command = [str(relinker), 'unused-filter=0', '--executable', 'picker/second.bin'] + if windows: + command += ['--windows'] + run = subprocess.run(command + [str(title / 'eboot.bin'), str(app)], capture_output=True, timeout=10) + assert run.returncode == 0, (run.stdout, run.stderr) + second = output / ('picker.second.bin.exe' if windows else 'picker.second.bin.elf') + assert second.is_file(), list(output.iterdir()) + shutil.copytree(title, output / 'app0') + (output / 'app0' / 'working').mkdir() + libraries = output / 'libs' + libraries.mkdir() + for library in [host, *dependencies]: + shutil.copy2(library, libraries / library.name) + if windows: + for name in ('libgcc_s_seh-1.dll', 'libstdc++-6.dll', 'libwinpthread-1.dll'): + shutil.copy2(toolchain / name, libraries / name) + else: + app.chmod(0o755) + second.chmod(0o755) + environment = {**os.environ, 'ANYPS5_LOADEXEC_TEST_ROOT': str(output), + 'LD_LIBRARY_PATH': str(libraries), 'PATH': str(libraries) + os.pathsep + os.environ['PATH']} + for quoted in (False, True): + events_path = output / 'events.txt' + events_path.unlink(missing_ok=True) + (output / 'pids.txt').unlink(missing_ok=True) + selected = dict(environment) + if quoted: + selected['ANYPS5_TEST_QUOTED_ARGV0'] = '1' + run = subprocess.run([str(app)], cwd=output, env=selected, capture_output=True, timeout=10) + events = (output / 'events.txt').read_text() if (output / 'events.txt').exists() else '' + assert events == 'first\nshutdown\nsecond\nshutdown\nreturned\n', (run.returncode, events, run.stdout, run.stderr) + assert run.returncode == (0 if windows else 37), (run.returncode, events, run.stdout, run.stderr) + pids = (output / 'pids.txt').read_text().splitlines() + assert len(pids) == 3 and len(set(pids)) == (3 if windows else 1), pids + print('LoadExec arguments: first -> second -> first; entry/getargv arguments, Unicode, cwd/resources, ' + 'rejected targets, shutdown without guest or host exit handlers passed') + finally: + remove_tree(directory) if __name__ == '__main__': diff --git a/core/relinker/relinker/tests/test_macos_guest_weak_imports.py b/core/relinker/relinker/tests/test_macos_guest_weak_imports.py new file mode 100644 index 0000000000..7a0f2dc432 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_guest_weak_imports.py @@ -0,0 +1,142 @@ +import os +import platform +import shutil +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +from test_guest_intel_trampolines import main_fixture +from test_guest_symbol_names import guest_module, string_table +from test_macos_output import MachO + +STB_GLOBAL = 1 +STB_WEAK = 2 +OPTIONAL = 'AAAAAAAAAAA' +REQUIRED = 'BBBBBBBBBBB' +ENTRY = 'ProbeOptional' +HOST = 'libWeakHost.prx' + + +def macos_guest_module(binding): + image = guest_module([(OPTIONAL, False), (REQUIRED, False), (ENTRY, True)]) + strings, offsets = string_table((OPTIONAL, REQUIRED, ENTRY, HOST)) + image[0x800:0x800 + len(strings)] = strings + struct.pack_into('\n' + '#include \n' + '\n' + 'int main(int argc, char **argv) {\n' + ' if (argc == 1) return 0;\n' + ' void *module = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);\n' + ' if (!module) { fprintf(stderr, "%s\\n", dlerror()); return 1; }\n' + ' dlerror();\n' + ' int (*entry)(void) = (int (*)(void))dlsym(module, argv[2]);\n' + ' const char *error = dlerror();\n' + ' if (error) { fprintf(stderr, "%s\\n", error); return 2; }\n' + ' printf("%d\\n", entry());\n' + ' return 0;\n' + '}\n' + ) + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', str(source), '-o', str(probe)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(probe)] + result = subprocess.run(command, capture_output=True, text=True, timeout=10) + if result.returncode != 0 and 'Bad CPU type in executable' in result.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the native x86_64 weak-import probe' + print('macOS guest weak imports: x86_64 execution unavailable') + return None + assert result.returncode == 0, (result.returncode, result.stdout, result.stderr) + kernel_source, kernel = work / 'kernel.c', work / 'libkernel.prx' + kernel_source.write_text('int Anyps5TestKernel(void) { return 0; }\n') + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(kernel_source), + '-Wl,-install_name,@rpath/libkernel.prx', '-o', str(kernel)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + return command, kernel + + +def check_macos_guest_weak_imports(relinker, work): + native = macos_native_probe(work) + for name, binding, optional, required, expected, missing in ( + ('weak-missing', STB_WEAK, False, True, 42, None), + ('weak-present', STB_WEAK, True, True, 77, None), + ('required-missing', STB_WEAK, True, False, None, REQUIRED), + ('global-missing', STB_GLOBAL, False, True, None, OPTIONAL)): + case = work / ('macos-guest-' + name) + modules, libraries = case / 'sce_module', case / 'libs' + modules.mkdir(parents=True) + libraries.mkdir() + (modules / 'optional.prx').write_bytes(macos_guest_module(binding)) + source, output = case / 'input.elf', case / 'output' + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), '--macos', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + converted = case / 'app0/sce_module/optional.prx.guest.prx' + image = converted.read_bytes() + macho = MachO(image) + offset, size = macho.dyld_info[2:4] + stream = image[offset:offset + size] + for symbol, opcode in ((OPTIONAL, 0x41 if binding == STB_WEAK else 0x40), (REQUIRED, 0x40)): + at = stream.find(('_' + symbol).encode() + b'\0') + assert at > 0 and stream[at - 1] == opcode, (name, symbol, stream) + assert '@rpath/' + HOST in macho.dylibs, (name, macho.dylibs) + if native: + command, kernel = native + shutil.copyfile(kernel, libraries / 'libkernel.prx') + provider_source, host = case / 'provider.c', libraries / HOST + provider_source.write_text((f'int {OPTIONAL}(void) {{ return 77; }}\n' if optional else '') + + (f'int {REQUIRED}(void) {{ return 42; }}\n' if required else '')) + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(provider_source), + '-Wl,-install_name,@rpath/' + HOST, '-o', str(host)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (name, result.stdout, result.stderr) + run = subprocess.run([*command, str(converted), ENTRY + '#guest'], + capture_output=True, text=True, timeout=10) + if expected is None: + assert run.returncode != 0 and missing in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == 0 and run.stdout.strip() == str(expected), (name, run.returncode, run.stdout, run.stderr) + if name == 'weak-missing': + host.unlink() + run = subprocess.run([*command, str(converted), ENTRY + '#guest'], + capture_output=True, text=True, timeout=10) + assert run.returncode != 0 and HOST in run.stderr, (run.returncode, run.stdout, run.stderr) + if native: + print('macOS guest weak imports: five native x86_64 loader controls passed') + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-guest-weak-') as directory: + check_macos_guest_weak_imports(relinker, Path(directory)) + print('macOS guest weak import tests passed') + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_macos_output.py b/core/relinker/relinker/tests/test_macos_output.py index dfbe252bca..694ed2e2eb 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -125,6 +125,17 @@ def check_imports(data): check(bytes.fromhex("4883ec08") in code, "import: the guest code is missing from the image") +def check_runtime_dependencies(relinker, directory): + for needed, expected in ( + (b"libc.prx", ["@rpath/libc.prx", "@rpath/libkernel.prx"]), + (b"libkernel.prx", ["@rpath/libkernel.prx", "@rpath/libc.prx"]), + (b"libSceLibcInternal.prx", ["@rpath/libSceLibcInternal.prx", "@rpath/libkernel.prx", "@rpath/libc.prx"]), + (b"libSceVideoOut.prx", ["@rpath/libSceVideoOut.prx", "@rpath/libkernel.prx", "@rpath/libc.prx"])): + name = needed.decode().removesuffix(".prx") + dylibs = [dylib for dylib in check_structure(name, relink(relinker, directory, name, import_fixture(library=needed))).dylibs if dylib.startswith("@rpath/")] + check(dylibs == expected, f"{name}: dependencies {dylibs}, expected {expected}") + + def check_tls(data): check(FS_LOAD in bytes(tls_fixture()), "tls: the fixture no longer loads through %fs") macho = check_structure("tls", data) @@ -180,6 +191,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-macos-output-") as directory: check_imports(relink(relinker, directory, "import", import_fixture())) + check_runtime_dependencies(relinker, directory) check_tls(relink(relinker, directory, "tls", tls_fixture())) check_options(relinker, directory) check_runpath_lists(relinker, directory) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 8061ee1638..6c30aada25 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -491,6 +491,25 @@ def make_image(transfer, metadata, extent=None, body=None): return image +def make_eof_segment_prefix_image(): + image = bytearray(0x1000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into("> 4, info & 15) + return attributes + + +def load_weak_module(path, symbol, libraries): + return subprocess.run([sys.executable, __file__, '--load-weak-module', str(path), symbol], + env={**os.environ, 'LD_LIBRARY_PATH': str(libraries)}, + capture_output=True, text=True, timeout=10) + + +def check_guest_weak_imports(relinker, work): + linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' + for name, binding, optional, required, expected, missing in ( + ('weak-missing', STB_WEAK, False, True, 42, None), + ('weak-present', STB_WEAK, True, True, 77, None), + ('required-missing', STB_WEAK, True, False, None, REQUIRED), + ('global-missing', STB_GLOBAL, False, True, None, OPTIONAL)): + case = work / ('guest-' + name) + modules, libraries = case / 'sce_module', case / 'libs' + modules.mkdir(parents=True) + libraries.mkdir() + raw = modules / 'optional.prx' + raw.write_bytes(weak_guest_module(binding)) + (libraries / HOST).write_bytes(weak_host_provider(optional, required)) + source, output = case / 'input.elf', case / 'output.elf' + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + converted = case / 'app0/sce_module/optional.prx.guest.prx' + image = converted.read_bytes() + attributes = module_import_attributes(image) + assert attributes.get(OPTIONAL) == (binding, 2), (name, attributes) + assert attributes.get(REQUIRED) == (STB_GLOBAL, 2), (name, attributes) + assert needed_libraries(image) == [HOST], (name, needed_libraries(image)) + if linux: + for module, symbol in ((raw, ENTRY), (converted, ENTRY + '#guest')): + run = load_weak_module(module, symbol, libraries) + if expected is None: + assert run.returncode != 0 and 'undefined symbol: ' + missing in run.stderr, \ + (name, module, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == 0 and run.stdout.strip() == str(expected), \ + (name, module, run.returncode, run.stdout, run.stderr) + if name == 'weak-missing': + (libraries / HOST).unlink() + run = load_weak_module(converted, ENTRY + '#guest', libraries) + assert run.returncode != 0 and HOST in run.stderr and 'cannot open shared object file' in run.stderr, \ + (name, run.returncode, run.stdout, run.stderr) def weak_executable(owner, symbol): @@ -54,6 +176,17 @@ def macho_bind_opcodes(data): def main(): + if sys.argv[1] == '--load-weak-module': + import ctypes + try: + module = ctypes.CDLL(sys.argv[2], mode=os.RTLD_NOW | os.RTLD_LOCAL) + entry = getattr(module, sys.argv[3]) + entry.restype = ctypes.c_int + print(entry()) + except OSError as error: + print(error, file=sys.stderr) + raise SystemExit(1) + return relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix='anyps5-weak-imports-') as directory: work = Path(directory) @@ -96,6 +229,7 @@ def main(): assert result.returncode == 0, (name, result.stdout, result.stderr) opcodes = macho_bind_opcodes(output.read_bytes()) assert opcodes == expected, (name, opcodes) + check_guest_weak_imports(relinker, work) print('Weak import tests passed') diff --git a/core/relinker/relinker/tests/test_windows_tls_protected_entries.py b/core/relinker/relinker/tests/test_windows_tls_protected_entries.py new file mode 100644 index 0000000000..4b7f2bd674 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_tls_protected_entries.py @@ -0,0 +1,96 @@ +"""Keep independently reachable code entries out of Windows TLS jump patches.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + + +TLS_AND_FOLLOWER = bytes.fromhex("64 48 8b 08 b8 2a 00 00 00 c3") +SEPARATE_FUNCTION = bytes.fromhex("b8 2a 00 00 00 c3") + + +def make_image(root=None): + image = bytearray(0x1000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into("mesh ? &*request.graphics->mesh : nullptr; append(key, mesh != nullptr); if (mesh == nullptr) return; - for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize}) append(key, value); + for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize, mesh->outputPrimitiveSize}) append(key, value); } static void appendTessellation(std::vector& key, const RecompileRequest& request) { @@ -261,6 +261,7 @@ private: append(key, value.registrationWordLimit); append(key, value.vertexSubgroups); append(key, value.float16DenormPreserve); + append(key, value.spannedWaves); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 20c4e29f51..b43d75c3c9 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -518,6 +518,7 @@ void writeMeshConfiguration(Writer& writer, const MeshConfiguration& configurati writer.WriteU32(configuration.ldsSizeDwords); writer.WriteU32(configuration.provokingVertex); writer.WriteU32(configuration.esgsItemSize); + writer.WriteU32(configuration.outputPrimitiveSize); } MeshConfiguration readMeshConfiguration(Reader& reader, std::uint32_t version) { @@ -531,6 +532,7 @@ MeshConfiguration readMeshConfiguration(Reader& reader, std::uint32_t version) { configuration.ldsSizeDwords = reader.ReadU32(); configuration.provokingVertex = reader.ReadU32(); configuration.esgsItemSize = version >= 4u ? reader.ReadU32() : 4u; + if (version >= 31u) configuration.outputPrimitiveSize = reader.ReadU32(); return configuration; } @@ -776,7 +778,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(30u); + writer.WriteU32(32u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -801,6 +803,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.vertexSubgroups); writer.WriteBool(request.target.float16DenormPreserve); writer.WriteU32Span(request.context.linkedCallFallbacks); + writer.WriteBool(request.target.spannedWaves); return base64Encode(buffer); } @@ -809,7 +812,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 > 30u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 32u) 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); @@ -845,6 +848,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.linkedCallFallbacks = reader.ReadU32Vector(); result.request.context.linkedCallFallbacks = result.linkedCallFallbacks; } + if (version >= 32u) result.request.target.spannedWaves = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index c78d429445..3d80b21d8e 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1,6 +1,8 @@ #include "ControlFlow/Structurizer.hpp" #include "ControlFlow/ControlFlowHelpers.hpp" #include +#include +#include #include #include #include @@ -50,6 +52,79 @@ bool inPlaceIntersections() { return enabled; } +struct BlockSet { + std::vector words; + + static std::size_t wordCount(std::uint32_t count) { + return (static_cast(count) + 63u) / 64u; + } + + static BlockSet filled(std::uint32_t count, std::uint64_t fill) { + BlockSet set; + set.words.assign(wordCount(count), fill); + return set; + } + + static BlockSet none(std::uint32_t count) { + return filled(count, 0); + } + + static BlockSet all(std::uint32_t count) { + auto set = filled(count, ~std::uint64_t{0}); + if (const auto tail = count % 64u) set.words.back() &= (std::uint64_t{1} << tail) - 1u; + return set; + } + + static BlockSet only(std::uint32_t count, std::uint32_t id) { + auto set = none(count); + set.add(id); + return set; + } + + static BlockSet fromIds(std::uint32_t count, const std::vector& ids) { + auto set = none(count); + for (const auto id : ids) set.add(id); + return set; + } + + void add(std::uint32_t id) { + words[static_cast(id) / 64u] |= std::uint64_t{1} << (id % 64u); + } + + void intersect(const BlockSet& other) { + for (std::size_t i = 0; i < words.size(); ++i) words[i] &= other.words[i]; + } + + bool operator==(const BlockSet& other) const { + return words == other.words; + } + + std::vector ids() const { + std::vector out; + for (std::size_t word = 0; word < words.size(); ++word) { + auto bits = words[word]; + while (bits != 0) { + const auto bit = static_cast(std::countr_zero(bits)); + out.push_back(static_cast(word * 64u) + bit); + bits &= bits - 1u; + } + } + return out; + } +}; + +bool replaceSet(BlockSet& destination, BlockSet&& next) { + if (next == destination) return false; + destination = std::move(next); + return true; +} + +std::vector> analysisVectors(const std::vector& sets) { + std::vector> vectors(sets.size()); + for (std::size_t i = 0; i < sets.size(); ++i) vectors[i] = sets[i].ids(); + return vectors; +} + void intersectAnalysisSet(std::vector& values, const std::vector& other) { if (!inPlaceIntersections()) { values = intersectSorted(values, other); @@ -366,27 +441,41 @@ std::uint32_t joinWithinIteration(const ControlFlowGraph& graph, const NaturalLo if (!joins[trueTarget] || !joins[falseTarget]) { return globalMerge; } - std::vector> postDominators(graph.blocks.size()); - for (const auto blockId : joining) { - postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; - } + const auto count = static_cast(graph.blocks.size()); + std::vector> postDominators(count); const auto order = analysisOrder(graph, true); - for (bool changed = true; changed;) { - changed = false; - for (const auto blockId : order) { - if (!joins[blockId] || blockId == globalMerge) { - continue; - } - auto next = joining; - for (const auto successor : graph.FindBlock(blockId).successors) { - if (joins[successor]) { - next = intersectSorted(next, postDominators[successor]); + if (inPlaceIntersections()) { + const auto universe = BlockSet::fromIds(count, joining); + std::vector sets(count, BlockSet::none(count)); + for (const auto blockId : joining) sets[blockId] = blockId == globalMerge ? BlockSet::only(count, globalMerge) : universe; + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + if (!joins[blockId] || blockId == globalMerge) continue; + auto next = universe; + for (const auto successor : graph.FindBlock(blockId).successors) { + if (joins[successor]) next.intersect(sets[successor]); } + next.add(blockId); + changed = replaceSet(sets[blockId], std::move(next)) || changed; } - addSortedUnique(next, blockId); - if (next != postDominators[blockId]) { - postDominators[blockId] = std::move(next); - changed = true; + } + postDominators = analysisVectors(sets); + } else { + for (const auto blockId : joining) postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + if (!joins[blockId] || blockId == globalMerge) continue; + auto next = joining; + for (const auto successor : graph.FindBlock(blockId).successors) { + if (joins[successor]) next = intersectSorted(next, postDominators[successor]); + } + addSortedUnique(next, blockId); + if (next != postDominators[blockId]) { + postDominators[blockId] = std::move(next); + changed = true; + } } } } @@ -449,33 +538,47 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock return std::all_of(tail.begin(), tail.end(), [&](std::uint32_t member) { return graph.FindBlock(member).predecessors.size() == 1u; }); }; std::vector> kept(count); - std::vector> postDominators(count, allBlockIds(count)); for (const auto& block : graph.blocks) { for (const auto successor : block.successors) { - if (!returns(successor)) { - kept[block.id].push_back(successor); - } - } - if (kept[block.id].empty()) { - postDominators[block.id] = {block.id}; + if (!returns(successor)) kept[block.id].push_back(successor); } } const auto order = analysisOrder(graph, true); - for (bool changed = true; changed;) { - changed = false; - for (const auto blockId : order) { - const auto& block = graph.blocks[blockId]; - if (kept[block.id].empty()) { - continue; + std::vector> postDominators; + if (inPlaceIntersections()) { + std::vector sets(count, BlockSet::all(count)); + for (const auto& block : graph.blocks) { + if (kept[block.id].empty()) sets[block.id] = BlockSet::only(count, block.id); + } + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (kept[block.id].empty()) continue; + auto next = sets[kept[block.id].front()]; + for (std::size_t i = 1; i < kept[block.id].size(); ++i) next.intersect(sets[kept[block.id][i]]); + next.add(block.id); + changed = replaceSet(sets[block.id], std::move(next)) || changed; } - auto next = postDominators[kept[block.id].front()]; - for (std::size_t i = 1; i < kept[block.id].size(); ++i) { - intersectAnalysisSet(next, postDominators[kept[block.id][i]]); - } - addSortedUnique(next, block.id); - if (next != postDominators[block.id]) { - replaceAnalysisSet(postDominators[block.id], std::move(next)); - changed = true; + } + postDominators = analysisVectors(sets); + } else { + postDominators.assign(count, allBlockIds(count)); + for (const auto& block : graph.blocks) { + if (kept[block.id].empty()) postDominators[block.id] = {block.id}; + } + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (kept[block.id].empty()) continue; + auto next = postDominators[kept[block.id].front()]; + for (std::size_t i = 1; i < kept[block.id].size(); ++i) intersectAnalysisSet(next, postDominators[kept[block.id][i]]); + addSortedUnique(next, block.id); + if (next != postDominators[block.id]) { + replaceAnalysisSet(postDominators[block.id], std::move(next)); + changed = true; + } } } } @@ -532,41 +635,66 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur if (!joins[trueTarget] || !joins[falseTarget]) { return InvalidControlFlowId; } - std::vector> postDominators(count, allBlockIds(count)); - postDominators[join] = {join}; const auto order = analysisOrder(graph, true); - for (bool changed = true; changed;) { - changed = false; - for (const auto blockId : order) { - const auto& block = graph.blocks[blockId]; - if (!joins[block.id] || block.id == join) { - continue; - } - bool seeded = !reuse; - auto next = seeded ? allBlockIds(count) : std::vector{}; - for (const auto successor : block.successors) { - if (joins[successor]) { + std::vector> postDominators; + if (reuse && inPlaceIntersections()) { + std::vector sets(count, BlockSet::all(count)); + sets[join] = BlockSet::only(count, join); + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (!joins[block.id] || block.id == join) continue; + bool seeded = false; + auto next = BlockSet::none(count); + for (const auto successor : block.successors) { + if (!joins[successor]) continue; if (!seeded) { - next = postDominators[successor]; + next = sets[successor]; seeded = true; - } else if (reuse) { - intersectAnalysisSet(next, postDominators[successor]); } else { - next = intersectSorted(next, postDominators[successor]); + next.intersect(sets[successor]); } } + if (!seeded) next = BlockSet::all(count); + next.add(block.id); + changed = replaceSet(sets[block.id], std::move(next)) || changed; } - if (!seeded) next = allBlockIds(count); - if (reuse) { - addSortedUnique(next, block.id); - } else { - addUnique(next, block.id); - sortUnique(next); - } - if (next != postDominators[block.id]) { - if (reuse) replaceAnalysisSet(postDominators[block.id], std::move(next)); - else postDominators[block.id] = std::move(next); - changed = true; + } + postDominators = analysisVectors(sets); + } else { + postDominators.assign(count, allBlockIds(count)); + postDominators[join] = {join}; + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (!joins[block.id] || block.id == join) continue; + bool seeded = !reuse; + auto next = seeded ? allBlockIds(count) : std::vector{}; + for (const auto successor : block.successors) { + if (joins[successor]) { + if (!seeded) { + next = postDominators[successor]; + seeded = true; + } else if (reuse) { + intersectAnalysisSet(next, postDominators[successor]); + } else { + next = intersectSorted(next, postDominators[successor]); + } + } + } + if (!seeded) next = allBlockIds(count); + if (reuse) addSortedUnique(next, block.id); + else { + addUnique(next, block.id); + sortUnique(next); + } + if (next != postDominators[block.id]) { + if (reuse) replaceAnalysisSet(postDominators[block.id], std::move(next)); + else postDominators[block.id] = std::move(next); + changed = true; + } } } } @@ -2000,33 +2128,45 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, b void Structurizer::computeDominatorTree(ControlFlowGraph& graph) const { const auto count = static_cast(graph.blocks.size()); - const auto all = allBlockIds(count); - - for (auto& block : graph.blocks) { - block.dominators = block.id == graph.entryBlock ? std::vector{block.id} : all; + const auto order = analysisOrder(graph, false); + if (inPlaceIntersections()) { + std::vector sets; + sets.reserve(count); + for (const auto& block : graph.blocks) sets.push_back(block.id == graph.entryBlock ? BlockSet::only(count, block.id) : BlockSet::all(count)); + bool changed = true; + while (changed) { + changed = false; + for (const auto id : order) { + auto& block = graph.blocks[id]; + if (block.id == graph.entryBlock) continue; + BlockSet next = block.predecessors.empty() ? BlockSet::only(count, block.id) : sets[block.predecessors.front()]; + if (!block.predecessors.empty()) { + for (std::size_t i = 1; i < block.predecessors.size(); ++i) next.intersect(sets[block.predecessors[i]]); + next.add(block.id); + } + changed = replaceSet(sets[id], std::move(next)) || changed; + } + } + for (std::uint32_t id = 0; id < count; ++id) graph.blocks[id].dominators = sets[id].ids(); + return; } - const auto order = analysisOrder(graph, false); + const auto all = allBlockIds(count); + for (auto& block : graph.blocks) block.dominators = block.id == graph.entryBlock ? std::vector{block.id} : all; bool changed = true; while (changed) { changed = false; for (const auto id : order) { auto& block = graph.blocks[id]; - if (block.id == graph.entryBlock) { - continue; - } - + if (block.id == graph.entryBlock) continue; std::vector next; if (block.predecessors.empty()) { next = {block.id}; } else { next = graph.blocks[block.predecessors.front()].dominators; - for (std::size_t i = 1; i < block.predecessors.size(); ++i) { - intersectAnalysisSet(next, graph.blocks[block.predecessors[i]].dominators); - } + for (std::size_t i = 1; i < block.predecessors.size(); ++i) intersectAnalysisSet(next, graph.blocks[block.predecessors[i]].dominators); addSortedUnique(next, block.id); } - if (next != block.dominators) { replaceAnalysisSet(block.dominators, std::move(next)); changed = true; @@ -2072,14 +2212,30 @@ void Structurizer::detectNaturalLoops(ControlFlowGraph& graph) const { void Structurizer::computePostDominators(ControlFlowGraph& graph) const { const auto count = static_cast(graph.blocks.size()); - const auto all = allBlockIds(count); - - for (auto& block : graph.blocks) { - block.postDominators = block.successors.empty() ? std::vector{block.id} : all; + const auto order = analysisOrder(graph, true); + if (inPlaceIntersections()) { + std::vector sets; + sets.reserve(count); + for (const auto& block : graph.blocks) sets.push_back(block.successors.empty() ? BlockSet::only(count, block.id) : BlockSet::all(count)); + bool changed = true; + while (changed) { + changed = false; + for (const auto id : order) { + auto& block = graph.blocks[id]; + BlockSet next = block.successors.empty() ? BlockSet::only(count, block.id) : sets[block.successors.front()]; + if (!block.successors.empty()) { + for (std::size_t i = 1; i < block.successors.size(); ++i) next.intersect(sets[block.successors[i]]); + next.add(block.id); + } + changed = replaceSet(sets[id], std::move(next)) || changed; + } + } + for (std::uint32_t id = 0; id < count; ++id) graph.blocks[id].postDominators = sets[id].ids(); + return; } - const auto order = analysisOrder(graph, true); - + const auto all = allBlockIds(count); + for (auto& block : graph.blocks) block.postDominators = block.successors.empty() ? std::vector{block.id} : all; bool changed = true; while (changed) { changed = false; @@ -2090,12 +2246,9 @@ void Structurizer::computePostDominators(ControlFlowGraph& graph) const { next = {block.id}; } else { next = graph.blocks[block.successors.front()].postDominators; - for (std::size_t i = 1; i < block.successors.size(); ++i) { - intersectAnalysisSet(next, graph.blocks[block.successors[i]].postDominators); - } + for (std::size_t i = 1; i < block.successors.size(); ++i) intersectAnalysisSet(next, graph.blocks[block.successors[i]].postDominators); addSortedUnique(next, block.id); } - if (next != block.postDominators) { replaceAnalysisSet(block.postDominators, std::move(next)); changed = true; diff --git a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp index 4b3755e656..7396e8e96d 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp @@ -19,6 +19,7 @@ struct BindingAllocationResult : CompiledBindingLayout { std::vector specialization; std::vector bindings; std::vector pushConstants; + std::vector shaderDataUserWords; }; class BindingAllocator { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp index d2df4ed3cc..6cbf62c460 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 FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const; + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; private: diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index 77328b8672..f83da69525 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -38,6 +38,7 @@ struct ResourceCapture { // The walk's read addresses when the plan has pure flat slots (the leaf of each pure slot, // and every other read sorted and deduplicated); empty otherwise. SrtReadTrace readTrace; + SrtUserDataUse userDataUse; // APS5_PROFILE_DRAW: what CaptureResources spent resolving the source (the stage inputs, the // key over the code, the plan) before the walk. std::uint64_t sourceNanoseconds = 0; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 72fe2675ee..2ba98418bd 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -92,6 +92,7 @@ struct ShaderMeshInputInfo: ShaderWorkgroupInputInfo { std::uint32_t maxPrimitives = 0; std::uint32_t provokingVertex = 0; std::uint32_t esgsItemSize = 0; + std::uint32_t outputPrimitiveSize = 3; [[nodiscard]] std::uint32_t InputPrimitiveSize() const { return inputPrimitive == 1u ? 1u : inputPrimitive == 2u ? 2u : 3u; diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index 46dc84574f..8d345285de 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_SRTWALKER_HPP #include "IntermediateRepresentation/IrProgram.hpp" +#include "UserDataUse.hpp" #include #include @@ -35,6 +36,7 @@ struct SrtRuntime { SrtMemoryProbe accessible = nullptr; SrtMemoryProbe pendingWrite = nullptr; SrtMemoryBulkReader readWords = nullptr; + SrtUserDataUse* userDataUse = nullptr; }; enum class RuntimeValueType { diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 2139ae7a61..d666acd0f8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace ShaderRecompiler::Detail { @@ -171,13 +172,14 @@ public: BasicEvaluator(const BasicEvaluator&) = delete; BasicEvaluator& operator=(const BasicEvaluator&) = delete; - static bool MayEvaluate(const IrResourcePlan& program, IrValue* value); + static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } void ReportNullRootReads(std::uint32_t* sink) { _nullRoots = sink; } private: + static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes); static float Float32(std::uint64_t bits); static std::uint64_t Float32Bits(float value); @@ -193,6 +195,8 @@ private: BasicEvaluator* _cleanEvaluator = nullptr; TNode _activeMask; bool _masked = false; + const BasicEvaluator* _outer = nullptr; + const void* _firstLane = nullptr; InaccessibleRead* _inaccessible = nullptr; std::uint32_t* _nullRoots = nullptr; EvaluatedValues _cache; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 1910bb1531..1f6c140f8d 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -160,7 +160,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin } } if (!samplers.empty()) { - if (samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader sampler pairs exceed runtime heap capacity"); + if (samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader sampler pairs exceed runtime heap capacity: " + std::to_string(samplers.size() / 2u) + " samplers"); addBinding(next, DescriptorBindingKind::Samplers, std::move(samplers)); } if (usesGds(program)) { @@ -187,6 +187,9 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (imageTables) { addBinding(next, DescriptorBindingKind::ImageTable); } + if (std::ranges::any_of(info.images, [](const ImageResource& image) { return image.table != NoTable && image.resourceClass == ImageResourceClass::Storage && image.tableOperation == TableOperation::Supported; })) { + addBinding(next, DescriptorBindingKind::ImageStorageTable); + } if (next.ShaderDataDwords() != 0u && !next.UsesPushData()) { addBinding(next, DescriptorBindingKind::ShaderData); diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 3505f640f4..52643c8990 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -846,6 +846,22 @@ void ConstantFolder::Fold(IrProgram& program) const { Fold(program, program.BlockOrder()); } +bool ConstantFolder::FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const { + if (program.Resources().stage != IrShaderStage::Vertex || hostSubgroupSize != 32u || spirvVersion < 0x00010000u || spirvVersion >= 0x00010600u) return false; + bool changed = false; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* value : block->Instructions()) { + if (value->Opcode() != IrOpcode::SelectU32 || value->ArgumentCount() != 3u) continue; + const auto& half = resolveArg(*value, 0); + auto& low = resolveArg(*value, 1); + if (half.Opcode() != IrOpcode::ULessThan32 || half.ArgumentCount() != 2u || !isLaneIndex(resolveArg(half, 0)) || !isImmediateU32(resolveArg(half, 1), 32u) || !isImmediate(low, IrType::U32) || !isImmediate(resolveArg(*value, 2), IrType::U32)) continue; + replaceWith(*value, low); + changed = true; + } + } + return changed; +} + bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { std::vector words; for (IrBlock* block : program.BlockOrder()) { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index d988925a91..79e73779e2 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -41,6 +41,9 @@ DescriptorKind PhysicalKindFor(DescriptorBindingKind kind) { if (kind == DescriptorBindingKind::ImageTable) { return DescriptorKind::SampledImage; } + if (kind == DescriptorBindingKind::ImageStorageTable) { + return DescriptorKind::StorageImage; + } const ImageResourceClass imageClass = ImageBindingResourceClass(kind); if (imageClass == ImageResourceClass::Sampled) { return DescriptorKind::SampledImage; @@ -82,6 +85,9 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { if (kind == DescriptorBindingKind::ImageTableMap) { return DescriptorRole::ImageTableMap; } + if (kind == DescriptorBindingKind::ImageStorageTable) { + return DescriptorRole::ImageStorageTable; + } if (ImageBindingResourceClass(kind) != ImageResourceClass::None) { return DescriptorRole::GuestImages; } @@ -229,7 +235,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (!exact) break; if (pair.image != resource) continue; const auto& words = snapshot.samplers.at(pair.sampler).dwords; - exact = proof.samplers[pair.sampler] && info.samplers.at(pair.sampler).uses == SamplerUseExplicitLod && (words[1] & 0xfffu) == 0u && (words[2] & 0x3fffu) == 0u; + exact = proof.samplers[pair.sampler] && (((words[2] >> 26u) & 3u) == 0u || (info.samplers.at(pair.sampler).uses == SamplerUseExplicitLod && (words[1] & 0xfffu) == 0u && (words[2] & 0x3fffu) == 0u)); } proof.imagesAtBaseLevel[resource] = exact; } @@ -313,7 +319,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const bool present = descriptor.dwords[2] != 0u && (descriptor.dwords[0] != 0u || (descriptor.dwords[1] & 0xffffu) != 0u); if (buffer.descriptorFormatted && (format > static_cast(IrBufferFormat::Format32_32_32_32Float) || (present && format == 0u))) fail("buffer specialization has an unsupported format"); const auto misalignment = present ? descriptor.dwords[0] & 3u : 0u; - if (buffer.atomic && misalignment != 0u) fail("buffer atomic on a V# whose base is not DWORD aligned"); auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); @@ -494,7 +499,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } for (auto& binding : plan.bindings) { - if (binding.descriptor.role == DescriptorRole::ImageTable) binding.tableSamplerMasks = samplerMasks; + if (binding.descriptor.role == DescriptorRole::ImageTable || binding.descriptor.role == DescriptorRole::ImageStorageTable) binding.tableSamplerMasks = samplerMasks; } return plan; } @@ -525,6 +530,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con const auto& layout = compiled.layout; const bool needsShaderData = layout.UsesPushData() || plan.storageData; auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector{}; + allocation.shaderDataUserWords.clear(); + if (needsShaderData) { + for (const auto reg : layout.userDataRegisters) allocation.shaderDataUserWords.push_back(reg - userDataBase); + } std::vector bindings; bindings.reserve(plan.bindings.size()); std::size_t writtenHere = 0; @@ -559,6 +568,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con } break; case DescriptorRole::ImageTable: + case DescriptorRole::ImageStorageTable: if (tables == nullptr) fail("DescriptorBindingBuilder::Populate image table elements were not resolved"); physical.guestDescriptor.reserve(tables->elements.size() * 8u); for (const auto& element : tables->elements) { @@ -567,7 +577,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con shape.dimension = element.dimension; physical.imageShapes.push_back(ImageShapeForResource(shape)); physical.imageDepthCompare.push_back(element.depthCompare); - physical.imageWritten.push_back(false); + physical.imageWritten.push_back(ImageBindingResourceClass(element.kind) == ImageResourceClass::Storage); physical.imageAtomic.push_back(false); std::uint32_t samplers = 0; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 0cc812e22b..2fc6b495be 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -78,7 +78,7 @@ ImageType rawImageType(const DescriptorValue& descriptor) { bool nullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { if (nullImageDescriptor(descriptor)) return true; - return rawImageType(descriptor) == static_cast(0u) && image.table == NoTable && image.read && !image.written && !image.atomic; + return static_cast(rawImageType(descriptor)) <= 1u && image.table == NoTable && image.read && !image.written && !image.atomic; } IrBufferFormat rawImageFormat(const DescriptorValue& descriptor) { @@ -260,7 +260,6 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.numericClass = IrTextureNumericClass::Uint; } if ((rawSintStorage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { - if (!base.depthBitsCompatible) return rejectImage(PoisonReason::InvalidFormat, "stores 16-bit data to an image of a SINT format"); decoded.conversionFormat = format; } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { @@ -524,27 +523,42 @@ private: skip(NarrowSkip::Pending); return; } - for (std::uint64_t index = 0; index < positions; index++) { - const auto address = base + first + index * domain.stride; - if (!PageAccessible(address, base, base + size)) { - skip(NarrowSkip::Unmapped); - return; + if (runtime.readWords != nullptr && positions != 0u && domain.stride % 4u == 0u && domain.stride <= 64u) { + const auto words = static_cast(((positions - 1u) * domain.stride) / 4u + 1u); + for (std::uint64_t page = (base + first) & ~(PageBytes - 1u); page < base + first + words * 4u; page += PageBytes) { + if (!PageAccessible(std::max(page, base + first), base, base + size)) { + skip(NarrowSkip::Unmapped); + return; + } + } + if (keyWords.size() < words) keyWords.resize(words); + if (!runtime.readWords(runtime.userContext, base + first, keyWords.data(), words)) throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(base + first)); + const auto step = static_cast(domain.stride / 4u); + values.reserve(values.size() + positions); + for (std::size_t index = 0; index < words; index += step) values.push_back(keyWords[index]); + } else { + for (std::uint64_t index = 0; index < positions; index++) { + const auto address = base + first + index * domain.stride; + if (!PageAccessible(address, base, base + size)) { + skip(NarrowSkip::Unmapped); + return; + } + values.push_back(Read(address)); } - values.push_back(Read(address)); } if (positions != 0u) scanned.emplace_back(base, size); } records.states.assign(records.keys, RecordState::OutsideDomain); records.outside = false; + const std::uint32_t unwrapped = std::numeric_limits::max() / column.stride; for (const auto value : values) { const std::uint32_t relative = value * column.stride; - const bool aligned = relative % column.stride == 0u; - const auto record = relative / column.stride; - if (aligned && record < records.keys) { + const auto record = value <= unwrapped ? value : relative % column.stride == 0u ? relative / column.stride : records.keys; + if (record < records.keys) { records.states[record] = RecordState::Read; continue; } - if (column.address || ImageTableAbi::ScalarBufferDword(relative + column.addend, column.offset, records.size).has_value()) records.outside = true; + if (!records.outside && (column.address || ImageTableAbi::ScalarBufferDword(relative + column.addend, column.offset, records.size).has_value())) records.outside = true; } records.narrowed = true; for (const auto& [base, size] : scanned) tables.ranges.emplace_back(base, size); @@ -844,6 +858,7 @@ private: std::unordered_map wordsIndex; std::unordered_map pages; std::unordered_map wholePages; + std::vector keyWords; }; void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, ResourceSnapshot& snapshot, std::vector& activeSources) { @@ -1330,7 +1345,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe face.cube = true; modes.push_back(face); } - if (image.dimension == RdnaImageDimension::Dim2D && image.flatLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + if ((image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.flatLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { auto line = mode; line.dimension = RdnaImageDimension::Dim1D; modes.push_back(line); @@ -1387,7 +1402,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe } } } - if (storage && image.depthBitsCompatible && !image.packed && !image.atomic64 && ((image.written && !image.read) || image.atomic)) { + if (storage && !image.packed && !image.atomic64 && ((image.written && !image.read) || image.atomic)) { constexpr std::array formats{IrBufferFormat::Format32SInt, IrBufferFormat::Format32_32SInt, IrBufferFormat::Format32_32_32_32SInt, IrBufferFormat::Format16SInt, IrBufferFormat::Format8_8SInt, IrBufferFormat::Format16_16SInt, IrBufferFormat::Format8_8_8_8SInt, IrBufferFormat::Format16_16_16_16SInt}; for (const auto format : formats) { if (image.atomic && format != IrBufferFormat::Format32SInt) continue; @@ -1572,7 +1587,8 @@ void ResourceMaterializer::ApplyStaticInterface(IrProgram& program, bool nativeS if (image.table == NoTable) continue; tables = true; const auto modes = RuntimeImageModes(image); - if (modes.empty() || modes.size() > ImageTableAbi::MaxModes) image.tableOperation = TableOperation::Unsupported; + const bool storage = image.resourceClass == ImageResourceClass::Storage; + if (modes.empty() || modes.size() > ImageTableAbi::MaxModes || (storage && image.mipMode == ImageMipMode::DynamicStorage)) image.tableOperation = TableOperation::Unsupported; } for (const auto& pair : resources.info.sampledPairs) { if (pair.image >= images.size() || pair.sampler >= resources.info.samplers.size()) throw std::runtime_error("static sampled pair is out of range"); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index c7ca0ee15a..9ff45fc794 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1477,8 +1477,7 @@ private: continue; } image.table = next++; - const bool storage = image.resourceClass == ImageResourceClass::Storage || image.written || image.atomic; - if (storage || image.packed || image.byElements != 0u) { + if (image.atomic || image.packed || image.byElements != 0u) { image.tableOperation = TableOperation::Unsupported; } } @@ -1633,7 +1632,9 @@ private: const auto lineSampleFlags = memory.imageSampleFlags & ~RdnaImageSampleFlagCompare; const bool flatLineSample = op == IrOpcode::ImageSampleRaw && (lineSampleFlags == RdnaImageSampleFlagLevelZero || lineSampleFlags == 0u); const bool flatLineExplicitLod = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLod; - image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample || flatLineExplicitLod) && !memory.imagePacked && memory.imageByElements == 0u; + const bool flatLineLevelZero = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; + const bool flatLineAccess = image.dimension == RdnaImageDimension::Dim2DArray ? flatLineLevelZero || flatLineExplicitLod : flatLineLoad || flatLineSample || flatLineExplicitLod; + image.flatLineCompatible = image.flatLineCompatible && flatLineAccess && !memory.imagePacked && memory.imageByElements == 0u; image.flatVolumeLineCompatible = image.flatVolumeLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 190e6af479..5e2850b387 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -194,7 +195,18 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re std::uint64_t low = 0; std::uint64_t high = 0; std::uint64_t offset = 0; - if (!Arg(handle, 0, low) || !Arg(handle, 1, high) || !Arg(inst, 1, offset)) { + std::int32_t origin = -1; + const auto baseWord = [&] { + const TNode source = handle.Argument(0); + if (_runtime.userDataUse == nullptr || source.HasImmediate() || source.Opcode() != IrOpcode::GetUserData) return Arg(handle, 0, low); + const auto reg = RegIndex(static_cast(source.Argument(0).RegisterIndex())); + if (reg < _program.userDataBase || reg - _program.userDataBase >= _runtime.userData.size() || reg - _program.userDataBase >= SrtUserDataUse::TrackedWords) return Arg(handle, 0, low); + origin = static_cast(reg - _program.userDataBase); + low = _runtime.userData[reg - _program.userDataBase]; + _runtime.userDataUse->asBase |= std::uint64_t {1} << origin; + return true; + }; + if (!baseWord() || !Arg(handle, 1, high) || !Arg(inst, 1, offset)) { return false; } const auto base = ((high << 32u) | static_cast(low)) & AddressMask; @@ -229,6 +241,7 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re result = 0; return true; } + if (_runtime.userDataUse != nullptr) _runtime.userDataUse->reads.emplace_back(address, origin); if (_inaccessible != nullptr && _runtime.accessible != nullptr && !_runtime.accessible(_runtime.userContext, address, sizeof(std::uint32_t)) && LoadedFromMemory(*handle.Value())) { *_inaccessible = {inst.Value(), address}; return false; @@ -315,9 +328,13 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& } switch (value.Opcode()) { case IrOpcode::GetUserData: - case IrOpcode::GetShaderBase: - case IrOpcode::ReadFirstLane: - case IrOpcode::ReadConst: return true; + case IrOpcode::GetShaderBase: return true; + case IrOpcode::ReadFirstLane: return value.ArgumentCount() < 2 ? std::optional(false) : std::nullopt; + case IrOpcode::ReadConst: { + const auto* slot = value.ArgumentCount() < 2 || value.Argument(1) == nullptr ? nullptr : value.Argument(1)->Resolve(); + if (slot == nullptr || !slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= program.srtReads.size()) return false; + return std::nullopt; + } case IrOpcode::LoadAddressU32: case IrOpcode::ReadConstBuffer: return IsRawRead(program, value); case IrOpcode::Phi: return std::nullopt; @@ -342,10 +359,27 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& } template -bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value) { +bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask) { + std::unordered_set firstLanes; + return MayEvaluate(program, value, activeMask, firstLanes); +} + +template +bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes) { std::unordered_map known; - const auto requirements = [&program](IrValue* current) { + const auto takesActiveArm = [activeMask](const IrValue* current) { + return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && current->Argument(0)->Resolve() == activeMask; + }; + const auto requirements = [&program, &takesActiveArm](IrValue* current) { std::vector required; + if (takesActiveArm(current)) { + required.push_back(current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr); + return required; + } + if (current->Opcode() == IrOpcode::ReadConst) { + required.push_back(program.srtReads[current->Argument(1)->Resolve()->ImmediateU32()].value); + return required; + } if (current->IsPhi()) { required.push_back(ResolveInvariantPhi(program, current)); return required; @@ -372,15 +406,28 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* } return required; }; + std::unordered_set expanding; std::vector> pending{{value->Resolve(), false}}; while (!pending.empty()) { const auto [current, expanded] = pending.back(); pending.pop_back(); if (known.contains(current)) continue; - if (const auto alone = evaluableAlone(program, *current)) { + if (current->Opcode() == IrOpcode::ReadFirstLane && current->ArgumentCount() >= 2) { + auto* source = current->Argument(0) != nullptr ? current->Argument(0)->Resolve() : nullptr; + const auto* mask = current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr; + if (!firstLanes.insert(current).second) { + known.emplace(current, false); + continue; + } + known.emplace(current, source != nullptr && MayEvaluate(program, source, mask, firstLanes)); + firstLanes.erase(current); + continue; + } + if (const auto alone = takesActiveArm(current) ? std::nullopt : evaluableAlone(program, *current)) { known.emplace(current, *alone); continue; } + if (!expanded && !expanding.insert(current).second) continue; const auto required = requirements(current); if (!expanded) { pending.emplace_back(current, true); @@ -389,7 +436,10 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* } continue; } - known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { return argument != nullptr && known.at(argument); })); + known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { + const auto found = argument != nullptr ? known.find(argument) : known.end(); + return found != known.end() && found->second; + })); } return known.at(value->Resolve()); } @@ -407,14 +457,20 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul if (reg < _program.userDataBase || reg - _program.userDataBase >= _runtime.userData.size()) { return false; } + if (_runtime.userDataUse != nullptr) _runtime.userDataUse->asValue |= reg - _program.userDataBase < SrtUserDataUse::TrackedWords ? std::uint64_t {1} << (reg - _program.userDataBase) : ~std::uint64_t {0}; result = _runtime.userData[reg - _program.userDataBase]; return true; } case IrOpcode::GetShaderBase: result = _runtime.shaderBase; return true; case IrOpcode::Phi: return EvaluatePhi(inst, result); case IrOpcode::ReadFirstLane: { + for (const auto* outer = this; outer != nullptr; outer = outer->_outer) { + if (outer->_firstLane == inst.Value()) return false; + } const TNode mask = inst.Argument(1); BasicEvaluator active(_program, _runtime, _cleanFlatSlots, _cleanEvaluator, &mask); + active._outer = this; + active._firstLane = inst.Value(); active.ReportInaccessibleReads(_inaccessible); active.ReportNullRootReads(_nullRoots); return active.EvaluateWide(inst.Argument(0), result); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 32dcb23ba2..9bf473cf58 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -494,6 +494,12 @@ void prepareInterface(const RecompileRequest& request, IrProgram& program) { const bool nativeSampleOffsets = request.target.nonConstantImageOffsets && std::find(request.target.supportedCapabilities.begin(), request.target.supportedCapabilities.end(), spv::CapabilityImageGatherExtended) != request.target.supportedCapabilities.end(); resourceMaterializer.ApplyStaticInterface(program, nativeSampleOffsets); + constexpr ConstantFolder constantFolder; + if (request.shader.stage == ShaderStage::Vertex && constantFolder.FoldVertexLaneBounds(program, HostSubgroupSize(request), request.target.spirvVersion)) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + } + deadCodeEliminator.RemoveIdentities(program); constexpr RedundantValueEliminator redundantValueEliminator; static_cast(redundantValueEliminator.Eliminate(program)); @@ -524,11 +530,19 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.registrationModules = registration; targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; + targetOptions.spannedWaves = request.target.spannedWaves; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; result.variantId = nextVariantId(); - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + SpirvEmitInfo emitted; + try { + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted); + } catch (const SpannedWaveUnsupported& error) { + std::fprintf(stderr, "[spanned-wave] 0x%llx falls back to two lanes per invocation: %s\n", static_cast(request.shader.codeAddress), error.what()); + targetOptions.spannedWaves = false; + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + } if (registration && result.spirv.size() > request.target.registrationWordLimit) { registration = false; program.Metadata().bindingLayoutComplete = false; @@ -549,6 +563,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.imageMetadataDword = bindings.layout.ImageMetadataDword(); result.runtimeImageCount = bindings.layout.runtimeImageCount; result.hostSubgroupSize = HostSubgroupSize(request); + result.fullSubgroups = emitted.fullSubgroups; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -865,6 +880,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .vertexOffsetConflict = artifact.vertexOffsetConflict, .instanceOffsetConflict = artifact.instanceOffsetConflict, .hostSubgroupSize = artifact.hostSubgroupSize, + .fullSubgroups = artifact.fullSubgroups, .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, @@ -992,6 +1008,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil std::copy_n(runtimeSelectors.begin(), layout.runtimeStateDwords, data->guestDescriptor.begin() + first); } } + result.shaderDataUserWords = std::move(bindings.shaderDataUserWords); result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; if (!tables.map.empty()) { result.imageTablePoison.reserve(tables.poison.size()); @@ -1326,11 +1343,12 @@ namespace { // traced into the capture (ResourceCapture::readTrace). void materializeCapture(ResourceCapture& capture, const SrtRuntime& runtime) { const auto& plan = *capture.plan; + SrtRuntime traced = runtime; + traced.userDataUse = &capture.userDataUse; if (std::none_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; })) { - ResourceMaterializer{}.Materialize(plan, runtime, capture.snapshot); + ResourceMaterializer{}.Materialize(plan, traced, capture.snapshot); return; } - SrtRuntime traced = runtime; traced.readTrace = &capture.readTrace; ResourceMaterializer{}.Materialize(plan, traced, capture.snapshot); auto& other = capture.readTrace.otherReads; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 157ab11feb..d51f6b7acc 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -5,6 +5,7 @@ #include "PipelineSpecialization.hpp" #include #include +#include #include #include #include @@ -266,6 +267,7 @@ struct SpirvTarget { std::uint32_t registrationWordLimit = 200000u; bool vertexSubgroups = true; bool float16DenormPreserve = false; + bool spannedWaves = false; }; struct BindingLayout { @@ -302,6 +304,7 @@ struct MeshConfiguration { std::uint32_t ldsSizeDwords; std::uint32_t provokingVertex; std::uint32_t esgsItemSize = 0; + std::uint32_t outputPrimitiveSize = 3; }; struct TessellationConfiguration { @@ -391,7 +394,8 @@ enum class DescriptorRole { ShaderData, ImageTable, SamplerTable, - ImageTableMap + ImageTableMap, + ImageStorageTable }; struct DescriptorBinding { @@ -468,25 +472,64 @@ struct BarycentricEmulationLayout { [[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation); +// A 128-bit hash of a module's words (SharedSpirv::ContentHash), zero for an empty module. The +// mixing is pure integer arithmetic over the words in order, so the value is the same on every run, +// platform and compiler: a pipeline journal names modules by it across runs. +struct SpirvContentHash { + std::uint64_t low = 0; + std::uint64_t high = 0; + bool operator==(const SpirvContentHash&) const = default; +}; + +inline std::uint64_t HashSpirvWords(std::span words, std::uint64_t seed) { + constexpr std::uint64_t multiplier = 0x9e3779b97f4a7c15ull; + std::uint64_t hash = seed ^ (static_cast(words.size()) * multiplier); + const auto mix = [&](std::uint64_t chunk) { + chunk *= 0xff51afd7ed558ccdull; + chunk ^= chunk >> 32u; + hash = (hash ^ chunk) * multiplier; + hash = (hash << 27u) | (hash >> 37u); + }; + std::size_t index = 0; + for (; index + 2 <= words.size(); index += 2) mix(words[index] | (static_cast(words[index + 1]) << 32u)); + if (index < words.size()) mix(words[index] ^ 0x8000000000000000ull); + hash ^= hash >> 33u; + hash *= 0xff51afd7ed558ccdull; + hash ^= hash >> 33u; + hash *= 0xc4ceb9fe1a85ec53ull; + hash ^= hash >> 33u; + return hash; +} + // Compiled SPIR-V shared between a cached variant and every result materialized from it: results // are copied per dispatch and draw, so the words are reference counted and only duplicated when a // holder writes to them (tests and tools patch modules in place). Reads look like a vector. // The non-const data(), operator[], begin() and end() count as writes: on a shared holder (every // cached result) they clone the module, so a consumer that only reads takes the result by const // reference, or the per-dispatch copy this class removes comes back without a compiler hint. +// ContentHash is computed once per shared holder, on first use, and dropped by a write. class SharedSpirv { public: SharedSpirv() = default; - SharedSpirv(std::vector words) : words(std::make_shared>(std::move(words))) {} - SharedSpirv(std::shared_ptr owner, std::span view) : owner(std::move(owner)), view(view) {} + SharedSpirv(std::vector words) : holder(std::make_shared(std::move(words), nullptr, std::span{})) {} + SharedSpirv(std::shared_ptr owner, std::span view) : holder(std::make_shared(std::vector{}, std::move(owner), view)) {} SharedSpirv& operator=(std::vector other) { - words = std::make_shared>(std::move(other)); - owner.reset(); - view = {}; + holder = std::make_shared(std::move(other), nullptr, std::span{}); return *this; } - [[nodiscard]] std::span Words() const { return words ? std::span(*words) : view; } + [[nodiscard]] std::span Words() const { return holder ? holder->Words() : std::span{}; } + [[nodiscard]] SpirvContentHash ContentHash() const { + if (holder == nullptr) return {}; + if (holder->hashed.load(std::memory_order_acquire)) return {holder->hashLow.load(std::memory_order_relaxed), holder->hashHigh.load(std::memory_order_relaxed)}; + const auto words = holder->Words(); + if (words.empty()) return {}; + const SpirvContentHash hash{HashSpirvWords(words, 0x5eed5a1d00000001ull), HashSpirvWords(words, 0x5eed5a1d00000002ull)}; + holder->hashLow.store(hash.low, std::memory_order_relaxed); + holder->hashHigh.store(hash.high, std::memory_order_relaxed); + holder->hashed.store(true, std::memory_order_release); + return hash; + } [[nodiscard]] std::vector Copy() const { return {Words().begin(), Words().end()}; } [[nodiscard]] std::size_t size() const { return Words().size(); } [[nodiscard]] bool empty() const { return Words().empty(); } @@ -503,18 +546,27 @@ public: friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } private: + struct Holder { + Holder(std::vector words, std::shared_ptr owner, std::span view) : words(std::move(words)), owner(std::move(owner)), view(view) {} + [[nodiscard]] std::span Words() const { return owner ? view : std::span(words); } + std::vector words; + std::shared_ptr owner; + std::span view; + mutable std::atomic hashLow{0}; + mutable std::atomic hashHigh{0}; + mutable std::atomic hashed{false}; + }; + std::vector& Mutable() { - if (words == nullptr || words.use_count() != 1) { - words = std::make_shared>(Copy()); - owner.reset(); - view = {}; + if (holder == nullptr || holder.use_count() != 1 || holder->owner != nullptr) { + holder = std::make_shared(Copy(), nullptr, std::span{}); + } else { + holder->hashed.store(false, std::memory_order_relaxed); } - return *words; + return holder->words; } - std::shared_ptr> words; - std::shared_ptr owner; - std::span view; + std::shared_ptr holder; }; struct VertexInput { @@ -553,6 +605,7 @@ struct CompiledShaderArtifact { bool vertexOffsetConflict = false; bool instanceOffsetConflict = false; std::uint32_t hostSubgroupSize = 0; + bool fullSubgroups = false; std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; @@ -576,10 +629,43 @@ struct ShaderInvocation { std::uint32_t imageTableFaults = 0; }; +// What a Vulkan pipeline built from a result depends on besides the fixed-function state and the +// descriptor set layouts: the module words, the specialization constants handed to the driver, the +// subgroup width the module was laid out for, the workgroup memory slice and the bindings' shape. +// Two results with equal identities build equal pipelines, in this run or another: unlike the +// process-lifetime variant ids, the identity is the same on every run, so the pipeline stores key on +// it and a pipeline journal can name a stage across runs. Empty for a result without a module. +struct PipelineIdentity { + std::uint64_t low = 0; + std::uint64_t high = 0; + [[nodiscard]] bool Empty() const { return low == 0 && high == 0; } + bool operator==(const PipelineIdentity&) const = default; + auto operator<=>(const PipelineIdentity&) const = default; +}; + struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { bool cacheHit = false; std::uint32_t workgroupMemoryDwords = 0; + std::vector shaderDataUserWords; [[nodiscard]] std::uint64_t PipelineVariantId() const { return specializationId != 0 ? specializationId : variantId; } + [[nodiscard]] PipelineIdentity Identity() const { + const auto hash = spirv.ContentHash(); + if (hash == SpirvContentHash{}) return {}; + PipelineIdentity identity{hash.low, hash.high}; + const auto mix = [&](std::uint64_t value) { + identity.low = (identity.low ^ value) * 0x9e3779b97f4a7c15ull; + identity.low ^= identity.low >> 29u; + identity.high = (identity.high ^ (value + 0x7f4a7c159e3779b9ull)) * 0xc2b2ae3d27d4eb4full; + identity.high ^= identity.high >> 31u; + }; + mix(specialization.size()); + for (const auto& constant : specialization) mix(constant.id | (static_cast(constant.value) << 32u)); + mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (static_cast(workgroupMemoryDwords) << 32u)); + mix(bindings.size()); + for (const auto& binding : bindings) mix(binding.descriptorSet | (static_cast(binding.binding) << 16u) | (static_cast(binding.kind) << 40u) | (static_cast(binding.count) << 48u)); + if (identity.Empty()) identity.low = 1; + return identity; + } }; [[nodiscard]] RecompileResult Recompile(const RecompileRequest& request); @@ -598,6 +684,7 @@ void SetDebugProbeActive(bool active); struct RectListShaders { RecompileResult control; RecompileResult evaluation; + RecompileResult lastVertexControl; }; [[nodiscard]] RectListShaders BuildRectListShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 99358baf0d..8fe2cea3f1 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -9,7 +9,7 @@ namespace ShaderRecompiler::RuntimeAbi { -inline constexpr std::uint32_t Version = 12u; +inline constexpr std::uint32_t Version = 14u; inline constexpr std::uint32_t DescriptorSet = 0u; inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; @@ -30,7 +30,8 @@ enum class Binding : std::uint32_t { SamplerTable = 63u, ImageTableMap = 64u, Count = 65u, - ImageTable = 256u + ImageTable = 256u, + ImageStorageTable = 257u }; enum class Stage : std::uint32_t { Main, Fragment, TessellationControl, TessellationEvaluation }; @@ -42,7 +43,7 @@ inline constexpr std::uint32_t SampledHeapCapacity = 64u; inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; -inline constexpr std::uint32_t SamplerHeapCapacity = 32u; +inline constexpr std::uint32_t SamplerHeapCapacity = 64u; inline constexpr std::uint32_t StateExportMappings = 0u; inline constexpr std::uint32_t StateDwords = 2u; inline constexpr std::uint32_t StateVertexSelectors = 0u; @@ -87,19 +88,21 @@ inline std::uint32_t HeapCapacity(Binding binding) { } static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(ResourceMetadata) == 48u); -static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(ShaderData) == 14528u); +static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(ShaderData) == 16064u); static_assert(UserDataDword == 4u && BufferOffsetsDword == 132u && DispatchThreadLimitDword == 164u); -static_assert(ExportMappingsDword == 3624u); +static_assert(ExportMappingsDword == 4008u); static_assert(offsetof(ResourceMetadata, descriptor) == 16u && offsetof(ShaderData, images) == 672u && offsetof(ShaderData, samplers) == 12960u); inline void RequireVersion(std::uint32_t version) { if (version != Version) throw std::runtime_error("Shader runtime ABI: incompatible version"); } -inline constexpr std::uint32_t ImageTableBinding = StageCount * static_cast(Binding::Count); +inline constexpr std::uint32_t ImageStorageTableBinding = StageCount * static_cast(Binding::Count); +inline constexpr std::uint32_t ImageTableBinding = ImageStorageTableBinding + 1u; inline std::uint32_t BindingNumber(Stage stage, Binding binding) { if (binding == Binding::ImageTable) return ImageTableBinding; + if (binding == Binding::ImageStorageTable) return ImageStorageTableBinding; const auto group = static_cast(stage); const auto index = static_cast(binding); const auto count = static_cast(Binding::Count); @@ -108,7 +111,7 @@ inline std::uint32_t BindingNumber(Stage stage, Binding binding) { } static_assert(FirstImageBinding + ImageBindingCount == static_cast(Binding::Samplers)); -static_assert(static_cast(Binding::ImageTable) > static_cast(Binding::Count) && ImageTableBinding == 260u); +static_assert(static_cast(Binding::ImageTable) > static_cast(Binding::Count) && ImageStorageTableBinding == 260u && ImageTableBinding == 261u); } diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index ee9393e078..84fbd1ae36 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -1,6 +1,7 @@ #ifndef CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP #define CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP +#include #include #include #include @@ -56,7 +57,12 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span(bytes.data()), static_cast(bytes.size())); + constexpr std::size_t largestWrite = std::size_t{1} << 30u; + for (std::size_t offset = 0; offset < bytes.size() && file;) { + const auto chunk = std::min(bytes.size() - offset, largestWrite); + file.write(reinterpret_cast(bytes.data() + offset), static_cast(chunk)); + offset += chunk; + } file.close(); written = !file.fail(); } diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3cedfd8604..f5fae0c986 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -84,7 +84,7 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 264, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); @@ -101,7 +101,7 @@ static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info enc static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); static_assert(sizeof(IrBindingLayout) == 80, "IrBindingLayout changed: update the layout encoder"); static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: update the layout encoder"); -static_assert(sizeof(BindingAllocationResult) == 160, "BindingAllocationResult changed: update the allocation encoder"); +static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 712, "IrResourcePlan changed: update EncodePlan and DecodePlan"); @@ -486,6 +486,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.vertexOffsetConflict); writer.Value(result.instanceOffsetConflict); writer.Value(result.hostSubgroupSize); + writer.Value(result.fullSubgroups); writer.Values(std::span(result.parameterExports)); writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) { out.Value(parameter.location); @@ -532,6 +533,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.vertexOffsetConflict); reader.Value(result.instanceOffsetConflict); reader.Value(result.hostSubgroupSize); + reader.Value(result.fullSubgroups); reader.Values(result.parameterExports); reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) { in.Value(parameter.location); @@ -1315,15 +1317,20 @@ LoadStatus unwrapEntry(std::span file, std::span runningWriters{0}; + class DiskStore { public: - DiskStore() { + explicit DiskStore(bool stopped) { const auto root = ShaderCacheDirectory(); if (root.empty()) return; this->root = root; directory = root / hex(SourceVersion()); discoverPrebuilt(); - std::thread([this] { run(); }).detach(); + stopping = stopped; + if (stopping) return; + writer = std::thread([this] { run(); }); + runningWriters.fetch_add(1, std::memory_order_acq_rel); } [[nodiscard]] bool Enabled() const { return !directory.empty(); } @@ -1367,6 +1374,19 @@ public: idle.wait(lock, [&] { return jobs.empty() && !writing; }); } + void Shutdown() { + std::thread stopped; + { + std::lock_guard lock(mutex); + stopping = true; + stopped = std::move(writer); + } + wake.notify_all(); + if (!stopped.joinable()) return; + if (stopped.get_id() == std::this_thread::get_id()) stopped.detach(); + else stopped.join(); + } + Counters Totals() const { Counters counters; counters.hits = hits.load(std::memory_order_relaxed); @@ -1524,7 +1544,12 @@ private: void enqueue(Job job) { { - std::lock_guard lock(mutex); + std::unique_lock lock(mutex); + if (stopping) { + lock.unlock(); + write(job); + return; + } jobs.push_back(std::move(job)); } wake.notify_one(); @@ -1535,7 +1560,7 @@ private: housekeeping(); std::unique_lock lock(mutex); while (true) { - wake.wait_for(lock, std::chrono::seconds(10), [&] { return !jobs.empty(); }); + wake.wait_for(lock, std::chrono::seconds(10), [&] { return !jobs.empty() || stopping; }); while (!jobs.empty()) { auto job = std::move(jobs.front()); jobs.pop_front(); @@ -1547,10 +1572,12 @@ private: writing = false; } idle.notify_all(); + if (stopping) break; lock.unlock(); report(false); lock.lock(); } + runningWriters.fetch_sub(1, std::memory_order_acq_rel); } void write(const Job& job) { @@ -1632,6 +1659,8 @@ private: std::condition_variable idle; std::deque jobs; bool writing = false; + bool stopping = false; + std::thread writer; std::atomic hits{0}, misses{0}, writes{0}, loadFailures{0}, writeFailures{0}, bytesRead{0}, bytesWritten{0}, loadNanoseconds{0}; std::atomic planHits{0}, planMisses{0}, planWrites{0}, planLoadFailures{0}; std::atomic prebuiltHits{0}, prebuiltMisses{0}, prebuiltRejected{0}, lookupNanoseconds{0}, readNanoseconds{0}, decodeNanoseconds{0}, maxLoadNanoseconds{0}, prebuiltBytesRead{0}, prebuiltLoadNanoseconds{0}; @@ -1641,8 +1670,24 @@ private: std::uint64_t reportedLoadNs = 0; }; +struct StoreLifecycle { + std::mutex mutex; + DiskStore* created = nullptr; + bool stopped = false; +}; + +StoreLifecycle& lifecycle() { + static auto* state = new StoreLifecycle(); + return *state; +} + DiskStore& store() { - static DiskStore* instance = new DiskStore(); + static DiskStore* instance = [] { + auto& state = lifecycle(); + std::lock_guard lock(state.mutex); + state.created = new DiskStore(state.stopped); + return state.created; + }(); return *instance; } @@ -1698,13 +1743,15 @@ void EncodeResult(const RecompileResult& result, std::vector& out) { encodeArtifact(writer, result); encodeInvocation(writer, result); writer.Value(result.workgroupMemoryDwords); + writer.Values(std::span(result.shaderDataUserWords)); } -bool DecodeResult(std::span bytes, RecompileResult& result) { - Reader reader(bytes); +bool DecodeResult(std::span bytes, RecompileResult& result, std::shared_ptr owner) { + Reader reader(bytes, std::move(owner)); decodeArtifact(reader, result); decodeInvocation(reader, result); reader.Value(result.workgroupMemoryDwords); + reader.Values(result.shaderDataUserWords); result.cacheHit = false; return reader.Done() && result.runtimeAbiVersion == RuntimeAbi::Version; } @@ -1799,6 +1846,21 @@ void Flush() { if (instance.Enabled()) instance.Flush(); } +void Shutdown() { + DiskStore* instance = nullptr; + { + auto& state = lifecycle(); + std::lock_guard lock(state.mutex); + state.stopped = true; + instance = state.created; + } + if (instance != nullptr) instance->Shutdown(); +} + +unsigned Writers() { + return runningWriters.load(std::memory_order_acquire); +} + Counters Totals() { return store().Totals(); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 1cce383ff3..f9840a0963 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -53,7 +53,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s [[nodiscard]] std::string EntryName(std::span key); void EncodeResult(const RecompileResult& result, std::vector& out); -[[nodiscard]] bool DecodeResult(std::span bytes, RecompileResult& result); +[[nodiscard]] bool DecodeResult(std::span bytes, RecompileResult& result, std::shared_ptr owner = nullptr); [[nodiscard]] std::vector EncodeEntry(std::span key, const CompiledVariant& variant); [[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner = nullptr); @@ -73,6 +73,8 @@ void Store(std::vector key, std::shared_ptr va [[nodiscard]] bool LoadPlan(std::span key, IrResourcePlan& plan); void StorePlan(std::vector key, std::shared_ptr plan); void Flush(); +void Shutdown(); +[[nodiscard]] unsigned Writers(); [[nodiscard]] Counters Totals(); } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 9d22da5885..e4a861594f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -5,6 +5,7 @@ #include "Optimization/BindingAllocator.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include +#include #include #include #include @@ -24,12 +25,21 @@ struct SpirvTargetOptions { bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool spannedWaves = false; +}; + +struct SpirvEmitInfo { + bool fullSubgroups = false; +}; + +struct SpannedWaveUnsupported : std::runtime_error { + using std::runtime_error::runtime_error; }; class SpirvEmitter { public: [[nodiscard]] std::vector Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const; - [[nodiscard]] std::vector Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const; + [[nodiscard]] std::vector Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target, SpirvEmitInfo* info = nullptr) const; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index dfc86a7f4b..f92f1e97c9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -101,6 +101,8 @@ struct SpirvEmitterState { std::uint32_t laneCount = 1; std::uint32_t hostSubgroupSize = 0; bool splitSubgroup = false; + bool spannedWave = false; + std::uint32_t spannedExchangeVariable = 0; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool registrationModules = false; @@ -165,6 +167,8 @@ struct SpirvEmitterState { std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; std::array vectorWindowVariable {}; + std::uint32_t vectorWindowIndexed = 0; + std::array, 2> vectorWindowSlots {}; std::array imageVariables {}; std::uint32_t samplerVariable = 0; std::vector tableImageVariables; @@ -184,6 +188,7 @@ struct SpirvEmitterState { std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; + std::uint32_t subgroupIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; @@ -210,6 +215,7 @@ struct SpirvValueEmitContext { std::uint32_t Ballot(const IrValue* predicate); std::uint32_t FirstLane(std::uint32_t ballot); std::uint32_t Shuffle(const IrValue& inst, std::size_t index, std::uint32_t lane); + std::uint32_t ShuffleInHalf(const IrValue& inst, std::size_t index, std::uint32_t lane); std::uint32_t Result(const IrValue& inst); std::uint32_t Define(const IrValue& inst, std::uint32_t value); std::uint32_t ResourceIndex(const IrValue* value, IrOpcode opcode); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp index b38bd34c1e..a24284d37c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp @@ -8,8 +8,10 @@ namespace ShaderRecompiler { bool HostSubgroupSingleLane(const SpirvEmitterState& state); void EmitSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate); +void EmitHostSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate); void EmitSubgroupBallotFindLsb(SpirvEmitterState& state, std::uint32_t result, std::uint32_t ballot); void EmitSubgroupShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane); +void EmitSubgroupShuffleInHalf(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane); void EmitSubgroupElect(SpirvEmitterState& state, std::uint32_t result); std::uint32_t EmitSubgroupLocalInvocationId(SpirvEmitterState& state); std::uint32_t EmitHostSubgroupLane(SpirvEmitterState& state, std::uint32_t lane); diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index 59757fa18c..300b471eef 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -56,7 +56,7 @@ public: } } - std::vector EmitControl() { + std::vector EmitControl(std::uint32_t flatVertex) { defineEntry(spv::ExecutionModelTessellationControl); const auto floatOne = constant(floatType, std::bit_cast(1.0f)); @@ -123,11 +123,11 @@ public: store(access(ptrOutputVec4Float, outputs[i], invocation), result(spv::OpCompositeConstruct, vec4FloatType, zero, zero, zero, zero)); continue; } - const auto input0 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(0))); if (parameters[i].flat) { - store(access(ptrOutputVec4Float, outputs[i], invocation), input0); + store(access(ptrOutputVec4Float, outputs[i], invocation), load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(flatVertex)))); continue; } + const auto input0 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(0))); const auto input1 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(1))); const auto input2 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(2))); const auto input3 = interpolate(input0, input1, input2, barycentric); @@ -398,7 +398,7 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding); RectListShaders shaders; - shaders.control.spirv = control.EmitControl(); + shaders.control.spirv = control.EmitControl(0u); shaders.control.bindings.push_back({DescriptorKind::StorageBuffer, DescriptorRole::FaultBuffer, 0, faultBinding, 1, {}}); shaders.control.bdaAbiVersion = BdaAbi::Version; shaders.evaluation.spirv = evaluation.EmitEvaluation(); @@ -406,6 +406,14 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); #endif + shaders.lastVertexControl = shaders.control; + if (std::any_of(parameters.begin(), parameters.end(), [](const Parameter& parameter) { return parameter.flat && !parameter.missing; })) { + RectListEmitter lastVertexControl(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); + shaders.lastVertexControl.spirv = lastVertexControl.EmitControl(2u); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shaders.lastVertexControl.spirv = ValidateAndOptimizeSpirv(shaders.lastVertexControl.spirv, target.vulkanVersion, target.spirvVersion); +#endif + } return shaders; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 757a979fa5..6ecc15534e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -186,11 +186,10 @@ std::uint32_t EmitDsMaskedLaneRead(SpirvEmitterState& state, std::uint32_t sourc if (state.laneCount == 2) { lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), lane, ConstantU32(state, 31u)); } - const auto physicalLane = EmitHostSubgroupLane(state, lane); const auto shuffled = state.module.AllocateId(); - EmitSubgroupShuffle(state, TypeU32(state), shuffled, source, physicalLane); + EmitSubgroupShuffleInHalf(state, TypeU32(state), shuffled, source, lane); const auto sourceExec = state.module.AllocateId(); - EmitSubgroupShuffle(state, TypeBool(state), sourceExec, exec, physicalLane); + EmitSubgroupShuffleInHalf(state, TypeBool(state), sourceExec, exec, lane); const auto sourceActive = Binary(state, spv::OpLogicalAnd, TypeBool(state), sourceExec, EmitSubgroupLaneActiveBool(state, lane)); return Select(state, TypeU32(state), sourceActive, shuffled, ConstantU32(state, 0u)); } @@ -389,7 +388,7 @@ private: bool everyInvocationEnters; std::unordered_set phis; - std::uint32_t budget = 512u; + std::uint32_t budget = 2048u; }; bool EveryLaneBit(const IrValue* bit, bool constantTrue) { @@ -566,7 +565,7 @@ std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStag std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { auto& state = ctx.state; - if (state.program.WaveSize() != 64u || (lane & 31u) != 31u) return std::nullopt; + if (state.program.WaveSize() != 64u || (lane & 31u) != 31u || state.spannedWave) return std::nullopt; const auto stage = state.program.Resources().stage; auto scan = MatchHalfWaveScan(inst.Argument(0), stage, false); const bool narrow = !scan && HostSubgroupNarrowerThanWave(state) && state.hostSubgroupSize == 32u; @@ -969,7 +968,7 @@ std::uint32_t EmitDppMoveU32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto flags = inst.Flags(); const auto target = EmitDppTargetLane(state, flags.control); - const auto shuffled = ctx.Shuffle(inst, 0, target.lane); + const auto shuffled = ctx.ShuffleInHalf(inst, 0, target.lane); if (flags.fetchInactive) { return EmitNative(state, target.valid, shuffled, ConstantU32(state, 0u)); } @@ -1079,7 +1078,7 @@ std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst) state.module.AddFunction(spv::OpShiftRightLogical, TypeU32(state), shifted, selected, shift); state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), index, shifted, ConstantU32(state, 15u)); state.module.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, rowValue, index); - const auto shuffled = ctx.Shuffle(inst, 0, target); + const auto shuffled = ctx.ShuffleInHalf(inst, 0, target); if (flags.fetchInactive) { return shuffled; } @@ -1107,9 +1106,9 @@ std::uint32_t EmitPermuteU32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto result = ConstantU32(state, 0u); for (std::uint32_t source = 0; source < 32u; ++source) { const auto from = Binary(state, spv::OpBitwiseOr, TypeU32(state), base, ConstantU32(state, source)); - const auto value = ctx.Shuffle(inst, 0, from); - const auto address = ctx.Shuffle(inst, 1, from); - const auto active = ctx.Shuffle(inst, 2, from); + const auto value = ctx.ShuffleInHalf(inst, 0, from); + const auto address = ctx.ShuffleInHalf(inst, 1, from); + const auto active = ctx.ShuffleInHalf(inst, 2, from); const auto target = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)), ConstantU32(state, 31u)); const auto hit = Binary(state, spv::OpLogicalAnd, TypeBool(state), active, Binary(state, spv::OpIEqual, TypeBool(state), target, own)); result = Select(state, TypeU32(state), hit, value, result); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp index f24270d401..f38c0fe047 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp @@ -1,7 +1,10 @@ #include "SpirvBackend/SpirvAluEmitter.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include +#include #include +#include namespace ShaderRecompiler { @@ -45,21 +48,89 @@ void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } -std::uint32_t VectorWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { - auto& state = ctx.state; +namespace { + +std::uint32_t VectorWindowElement(SpirvEmitterState& state, std::uint32_t index) { const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), ctx.Arg(inst, 0u)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), index); return pointer; } +std::uint32_t AccessNarrowedWindow(SpirvEmitterState& state, std::uint32_t index, const std::function& access) { + const auto& slots = state.vectorWindowSlots.at(state.laneHalf); + const auto arrayLabel = state.module.AllocateId(); + const auto slotsLabel = state.module.AllocateId(); + const auto slotsMerge = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto inArray = Binary(state, spv::OpULessThan, TypeBool(state), index, ConstantU32(state, state.vectorWindowIndexed)); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, inArray, arrayLabel, slotsLabel); + EmitLabel(state, arrayLabel); + const auto fromArray = access(VectorWindowElement(state, index)); + const auto arrayExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, slotsLabel); + std::vector labels(slots.size()); + std::vector words{spv::OpSwitch, index, slotsMerge}; + for (std::size_t slot = 0; slot < slots.size(); ++slot) { + labels[slot] = state.module.AllocateId(); + words.push_back(state.vectorWindowIndexed + static_cast(slot)); + words.push_back(labels[slot]); + } + state.module.AddFunction(spv::OpSelectionMerge, slotsMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(std::span(words)); + std::vector incoming; + for (std::size_t slot = 0; slot < slots.size(); ++slot) { + EmitLabel(state, labels[slot]); + incoming.push_back(access(slots[slot])); + incoming.push_back(labels[slot]); + state.module.AddFunction(spv::OpBranch, slotsMerge); + } + incoming.push_back(ConstantU32(state, 0u)); + incoming.push_back(slotsLabel); + EmitLabel(state, slotsMerge); + const auto fromSlots = state.module.AllocateId(); + std::vector phi{spv::OpPhi, TypeU32(state), fromSlots}; + phi.insert(phi.end(), incoming.begin(), incoming.end()); + state.module.AddFunction(std::span(phi)); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeU32(state), value, fromArray, arrayExit, fromSlots, slotsMerge); + return value; +} + +std::uint32_t DirectWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const IrValue* index = inst.Argument(0)->Resolve(); + if (state.vectorWindowIndexed == 0u) return VectorWindowElement(state, ctx.Arg(inst, 0u)); + if (!index->HasImmediate()) return 0u; + if (index->ImmediateU32() < state.vectorWindowIndexed) return VectorWindowElement(state, ctx.Arg(inst, 0u)); + return state.vectorWindowSlots.at(state.laneHalf).at(index->ImmediateU32() - state.vectorWindowIndexed); +} + +} + void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { - const auto value = ctx.state.module.AllocateId(); - ctx.state.module.AddFunction(spv::OpLoad, TypeU32(ctx.state), value, VectorWindowElement(ctx, inst)); - ctx.Define(inst, value); + auto& state = ctx.state; + const auto load = [&state](std::uint32_t pointer) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto pointer = DirectWindowElement(ctx, inst); + ctx.Define(inst, pointer != 0u ? load(pointer) : AccessNarrowedWindow(state, ctx.Arg(inst, 0u), load)); } void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { - ctx.state.module.AddFunction(spv::OpStore, VectorWindowElement(ctx, inst), ctx.Arg(inst, 1u)); + auto& state = ctx.state; + const auto value = ctx.Arg(inst, 1u); + const auto store = [&state, value](std::uint32_t pointer) { + state.module.AddFunction(spv::OpStore, pointer, value); + return ConstantU32(state, 0u); + }; + if (const auto pointer = DirectWindowElement(ctx, inst); pointer != 0u) (void)store(pointer); + else (void)AccessNarrowedWindow(state, ctx.Arg(inst, 0u), store); } void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 4b0d60d921..3ae3e3744e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include #include namespace ShaderRecompiler { @@ -115,7 +116,14 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) { if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } - state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); + if (state.program.Resources().stage != IrShaderStage::Pixel) { + state.module.AddFunction(spv::OpReturn); + } else if (std::ranges::find(state.supportedExtensions, "SPV_KHR_terminate_invocation") != state.supportedExtensions.end()) { + state.module.EmitExtension("SPV_KHR_terminate_invocation"); + state.module.AddFunction(spv::OpTerminateInvocation); + } else { + state.module.AddFunction(spv::OpKill); + } } EmitLabel(state, next); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 2bd877635a..0d629adaca 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -173,6 +173,13 @@ std::uint32_t SpirvValueEmitContext::Shuffle(const IrValue& inst, std::size_t in return value; } +std::uint32_t SpirvValueEmitContext::ShuffleInHalf(const IrValue& inst, std::size_t index, std::uint32_t lane) { + if (!state.spannedWave) return Shuffle(inst, index, lane); + const auto result = state.module.AllocateId(); + EmitSubgroupShuffleInHalf(state, TypeId(state, inst.Argument(index)->Type()), result, Arg(inst, index), lane); + return result; +} + std::uint32_t SpirvValueEmitContext::Result(const IrValue& inst) { if (const auto found = definitions.find(&inst); found != definitions.end()) { return found->second; @@ -233,7 +240,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Co return Emit(program, ShaderStageInputInfo {}, bindings, target); } -std::vector SpirvEmitter::Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const { +std::vector SpirvEmitter::Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target, SpirvEmitInfo* info) const { if (program.Resources().stage != IrShaderStage::Compute && program.Resources().stage != IrShaderStage::Vertex && program.Resources().stage != IrShaderStage::Pixel && program.Resources().stage != IrShaderStage::Mesh && program.Resources().stage != IrShaderStage::Local && program.Resources().stage != IrShaderStage::TessellationControl && program.Resources().stage != IrShaderStage::TessellationEvaluation) { FailProgram(program, "binary SPIR-V emitter received an unsupported shader stage"); } @@ -262,23 +269,31 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; const auto* workgroup = ShaderWorkgroupInputFor(state); - state.laneCount = workgroup != nullptr && program.WaveSize() == 64u && workgroup->hostSubgroupSize == 32u ? 2u : 1u; + const bool halfHost = workgroup != nullptr && program.WaveSize() == 64u && workgroup->hostSubgroupSize == 32u; + state.spannedWave = halfHost && target.spannedWaves && program.Resources().stage == IrShaderStage::Compute && !state.requirements.computeDerivatives && std::max(workgroup->threadsNum[0], 1u) * std::max(workgroup->threadsNum[1], 1u) * std::max(workgroup->threadsNum[2], 1u) == 64u; + state.laneCount = halfHost && !state.spannedWave ? 2u : 1u; + if (state.spannedWave) { + state.requirements.subgroupLocalInvocationId = true; + state.requirements.subgroupBallot = true; + state.requirements.subgroupShuffle = true; + } if (state.laneCount == 2u) state.sharedLaneValues = WaveUniformValues(program); if (program.Resources().stage == IrShaderStage::Compute && workgroup != nullptr) { // The key comes from a subgroup ballot (ReadFirstLane), so the slot is uniform over the // workgroup only when the workgroup is one wave held by one host subgroup; a wave64 program // kept at one lane per invocation spans two subgroups (see WaveLdsScope). const auto threads = std::max(workgroup->threadsNum[0], 1u) * std::max(workgroup->threadsNum[1], 1u) * std::max(workgroup->threadsNum[2], 1u); - const bool oneSubgroup = state.laneCount == 2u || (program.WaveSize() == workgroup->hostSubgroupSize && workgroup->hostSubgroupSize <= 32u); + const bool oneSubgroup = state.laneCount == 2u || state.spannedWave || (program.WaveSize() == workgroup->hostSubgroupSize && workgroup->hostSubgroupSize <= 32u); state.tableIndexNonUniform = threads > program.WaveSize() || !oneSubgroup; } state.waveLdsScope = WaveLdsScope(program, workgroup, state.laneCount); if (const char* guard = std::getenv("APS5_LOOP_GUARD")) state.loopGuardLimit = static_cast(std::strtoul(guard, nullptr, 0)); // Stopped invocations would leave the wave LDS barriers incomplete. - state.bdaStopsInvocations = state.waveLdsScope == 0 && BdaInvocationsMayStop(program) && program.Resources().stage != IrShaderStage::Mesh; + state.bdaStopsInvocations = state.waveLdsScope == 0 && !state.spannedWave && BdaInvocationsMayStop(program) && program.Resources().stage != IrShaderStage::Mesh; EmitModuleHeader(state, bindings); EmitProgram(state); state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables); + if (info != nullptr) info->fullSubgroups = state.spannedWave; return state.module.Finalize(); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 0eb36a24bb..6b9f77a57e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -741,9 +741,6 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce auto& state = ctx.state; const auto& mem = access.mem; const auto sint = SintStorageFormat(access.image); - if (sint.format != IrBufferFormat::Invalid && mem.dataBits == 16u) { - ctx.Fail(access.inst, "stores 16-bit data to an image of a SINT format"); - } std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); for (std::uint32_t component = 0; component < 4u; component++) { @@ -766,7 +763,7 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); if (component < sint.componentCount) { - values[component] = EmitFormatStoreComponent(state, sint, component, values[component]); + values[component] = mem.dataBits == 16u ? EmitD16StoreComponent(state, sint, component, values[component]) : EmitFormatStoreComponent(state, sint, component, values[component]); } } const auto texel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 51c6548c8e..3063e2d37e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -75,9 +75,19 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id) { if (!state.tableIndexNonUniform) return; state.module.EmitCapability(spv::CapabilityShaderNonUniform); state.module.EmitCapability(spv::CapabilitySampledImageArrayNonUniformIndexing); + if (std::any_of(state.tableImageVariables.begin(), state.tableImageVariables.end(), [](const SpirvTableImageVariable& variable) { return ImageBindingResourceClass(variable.kind) == ImageResourceClass::Storage; })) state.module.EmitCapability(spv::CapabilityStorageImageArrayNonUniformIndexing); state.module.AddAnnotation(spv::OpDecorate, id, spv::DecorationNonUniform); } +std::uint32_t TableStorageImagePointer(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, std::uint32_t pointerType) { + const auto found = std::find_if(state.tableImageVariables.begin(), state.tableImageVariables.end(), [&](const SpirvTableImageVariable& variable) { return variable.kind == kind; }); + if (found == state.tableImageVariables.end() || !state.tableAccess.active) ExitDescriptorBindingFailure(state, kind, resource, "image storage table array was not emitted for the mode"); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, found->variable, state.tableAccess.element); + DecorateTableNonUniform(state, pointer); + return pointer; +} + bool TableModeEmitted(const ImageResource& mode) { return RdnaImageDimensionInfoFor(mode.dimension).multisampled == 0u && mode.packedFormat != IrBufferFormat::Fmask8_S2_F1; } @@ -216,8 +226,9 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 FailEmit("storage image descriptor requested for a non-storage image"); } const auto kind = DescriptorBindingForImage(image); - const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image)); + if (image.table != NoTable) return TableStorageImagePointer(state, kind, resource, pointerType); + const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } @@ -231,9 +242,17 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std state.module.EmitCapability(spv::CapabilityStorageImageWriteWithoutFormat); } const auto kind = DescriptorBindingForImage(image); - const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto imageType = ImageType(state, image); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, imageType); + if (image.table != NoTable) { + if (RdnaImageDimensionInfoFor(image.dimension).multisampled != 0u || image.mipMode == ImageMipMode::DynamicStorage) FailEmit("an image table store to a multisampled image or through dynamic mips is not implemented"); + const auto descriptor = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, imageType, descriptor, TableStorageImagePointer(state, kind, resource, pointerType)); + DecorateTableNonUniform(state, descriptor); + state.module.AddFunction(spv::OpImageWrite, descriptor, coord, texel); + return; + } + const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); const auto loadAt = [&](std::uint32_t index) { const auto pointer = DescriptorElementPointer(state, pointerType, variable, index, kind, resource, "storage image descriptor array was not emitted"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 168803014c..2f44307dc3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -165,7 +165,7 @@ void DefineInputs(SpirvEmitterState& state) { for (const auto& input : state.program.Info().inputs) { state.inputs.push_back(SpirvInputBinding {input}); } - if (state.laneCount == 2u) { + if (state.laneCount == 2u || state.spannedWave) { const auto addBuiltin = [&](StageInputKind kind, std::uint32_t components, const char* name) { if (std::none_of(state.inputs.begin(), state.inputs.end(), [kind](const SpirvInputBinding& input) { return input.kind == kind; @@ -293,6 +293,10 @@ void DefineInputs(SpirvEmitterState& state) { if (state.requirements.subgroupLocalInvocationId) { const auto variable = DefineInterfaceVariable(state, TypeU32(state), spv::StorageClassInput, "gl_SubgroupInvocationID"); state.subgroupLocalInvocationIdVariable = variable; + if (state.spannedWave) { + state.subgroupIdVariable = DefineInterfaceVariable(state, TypeU32(state), spv::StorageClassInput, "gl_SubgroupID"); + state.module.AddAnnotation(spv::OpDecorate, state.subgroupIdVariable, spv::DecorationBuiltIn, spv::BuiltInSubgroupId); + } state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, spv::BuiltInSubgroupLocalInvocationId); if (state.program.Resources().stage == IrShaderStage::Pixel) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat); @@ -467,17 +471,20 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.gdsVariable, spv::DecorationCoherent); break; case DescriptorBindingKind::ImageTable: + case DescriptorBindingKind::ImageStorageTable: RequireTableIndexing(state); + if (binding.kind == DescriptorBindingKind::ImageStorageTable) state.module.EmitCapability(spv::CapabilityStorageImageArrayDynamicIndexing); for (std::uint32_t resource = 0; resource < state.program.Info().images.size(); ++resource) { const auto& image = state.program.Info().images[resource]; if (image.table == NoTable) continue; for (const auto& mode : state.program.Info().runtimeImageModes.at(resource)) { if (!TableModeEmitted(mode)) continue; const auto kind = DescriptorBindingForImage(mode); + if ((ImageBindingResourceClass(kind) == ImageResourceClass::Storage) != (binding.kind == DescriptorBindingKind::ImageStorageTable)) continue; if (std::any_of(state.tableImageVariables.begin(), state.tableImageVariables.end(), [&](const SpirvTableImageVariable& variable) { return variable.kind == kind; })) continue; const auto name = "table_images_" + std::to_string(static_cast(kind)); state.tableImageVariables.push_back({kind, Define(state.module.Type(spv::OpTypeRuntimeArray, ImageType(state, mode)), name.c_str(), spv::StorageClassUniformConstant)}); - if (mode.dimension == RdnaImageDimension::Dim1D || mode.dimension == RdnaImageDimension::Dim1DArray) state.module.EmitCapability(spv::CapabilitySampled1D); + if (mode.dimension == RdnaImageDimension::Dim1D || mode.dimension == RdnaImageDimension::Dim1DArray) state.module.EmitCapability(binding.kind == DescriptorBindingKind::ImageStorageTable ? spv::CapabilityImage1D : spv::CapabilitySampled1D); } } break; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 849e954bbe..4c05ffe094 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -196,6 +196,13 @@ namespace ShaderRecompiler } std::uint32_t EmitLocalInvocationIndex(SpirvEmitterState& state) { + if (state.spannedWave) { + const auto subgroup = state.module.AllocateId(); + const auto invocation = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), subgroup, state.subgroupIdVariable); + state.module.AddFunction(spv::OpLoad, TypeU32(state), invocation, state.subgroupLocalInvocationIdVariable); + return EmitAddU32(state, EmitBinaryU32(state, spv::OpShiftLeftLogical, subgroup, ConstantU32(state, 5u)), invocation); + } const auto variable = InputVariableForKind(state, StageInputKind::LocalInvocationIndex); if (variable == 0) { FailEmit("LocalInvocationIndex was not declared"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index b7ee109d7f..e152d36092 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1365,7 +1365,7 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std: state.module.AddFunction(spv::OpPhi, TypeBool(state), doneNext, served, servedMerge, done, body); state.module.AddFunction(spv::OpPhi, type, resultNext, servedResult, servedMerge, result, body); const auto ballot = state.module.AllocateId(); - EmitSubgroupBallot(state, ballot, Unary(state, spv::OpLogicalNot, TypeBool(state), doneNext)); + EmitHostSubgroupBallot(state, ballot, Unary(state, spv::OpLogicalNot, TypeBool(state), doneNext)); std::uint32_t remaining = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < 4u; ++component) { const auto word = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 7e4da050f6..763f1a1fa8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -5,6 +5,11 @@ #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include #include +#include +#include +#include +#include +#include #include #include @@ -41,6 +46,170 @@ std::uint32_t DefineInterfaceVariable(SpirvEmitterState& state, std::uint32_t ty return variable; } +namespace { + +class WindowIndexBounds { +public: + std::optional Of(const IrValue& input) { + const IrValue* value = input.Resolve(); + if (value->HasImmediate()) return value->ImmediateU32(); + if (const auto found = memo.find(value); found != memo.end()) return found->second; + if (!active.insert(value).second || active.size() > 256u) return std::nullopt; + const auto bound = compute(*value); + active.erase(value); + memo.emplace(value, bound); + return bound; + } + +private: + using Bound = std::optional; + + Bound maxOf(std::initializer_list bounds) { + std::uint64_t result = 0; + for (const auto& bound : bounds) { + if (!bound) return std::nullopt; + result = std::max(result, *bound); + } + return result; + } + + static std::vector reachingStores(const IrValue& load, std::uint32_t slot) { + std::vector values; + std::unordered_set visited; + std::vector> pending {{load.Parent(), &load}}; + while (!pending.empty()) { + const auto [block, before] = pending.back(); + pending.pop_back(); + const auto& instructions = block->Instructions(); + auto it = instructions.rbegin(); + if (before != nullptr) it = std::next(std::find(instructions.rbegin(), instructions.rend(), before)); + bool stored = false; + for (; it != instructions.rend() && !stored; ++it) { + if ((*it)->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = (*it)->Argument(0)->Resolve(); + if (!index->HasImmediate() || index->ImmediateU32() != slot) continue; + values.push_back((*it)->Argument(1)); + stored = true; + } + if (stored) continue; + for (IrBlock* predecessor : block->Predecessors()) { + if (visited.insert(predecessor).second) pending.emplace_back(predecessor, nullptr); + } + } + return values; + } + + Bound compute(const IrValue& value) { + const auto arg = [&](std::size_t index) { return Of(*value.Argument(index)); }; + const auto immediate = [&](std::size_t index) -> std::optional { + const IrValue* operand = value.Argument(index)->Resolve(); + if (!operand->HasImmediate()) return std::nullopt; + return operand->ImmediateU32(); + }; + const auto fits = [](std::uint64_t bound) -> Bound { return bound <= 0xffffffffull ? Bound(bound) : std::nullopt; }; + switch (value.Opcode()) { + case IrOpcode::BitFieldUExtract: { + const auto count = immediate(2); + if (count && *count < 32u) return (std::uint64_t {1} << *count) - 1u; + return arg(0); + } + case IrOpcode::BitwiseAnd32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (lhs && rhs) return std::min(*lhs, *rhs); + return lhs ? lhs : rhs; + } + case IrOpcode::UMin32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (!lhs || !rhs) return std::nullopt; + return std::min(*lhs, *rhs); + } + case IrOpcode::UMax32: + return maxOf({arg(0), arg(1)}); + case IrOpcode::BitwiseOr32: { + const auto both = maxOf({arg(0), arg(1)}); + if (!both) return std::nullopt; + std::uint64_t mask = 0; + while (mask < *both) mask = mask << 1u | 1u; + return mask; + } + case IrOpcode::IAdd32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (!lhs || !rhs) return std::nullopt; + return fits(*lhs + *rhs); + } + case IrOpcode::ShiftRightLogical32: { + const auto shift = immediate(1); + const auto bound = arg(0); + if (!bound) return std::nullopt; + return shift ? *bound >> (*shift & 31u) : *bound; + } + case IrOpcode::ShiftLeftLogical32: { + const auto shift = immediate(1); + const auto bound = arg(0); + if (!bound || !shift) return std::nullopt; + return fits(*bound << (*shift & 31u)); + } + case IrOpcode::ConvertU32U16: + return 0xffffu; + case IrOpcode::Select: + case IrOpcode::SelectU32: + { + if (value.Argument(0)->Resolve()->Opcode() == IrOpcode::GetExec || value.Argument(2)->Resolve()->Opcode() == IrOpcode::LoadVectorWindow) return arg(1); + const auto lhs = arg(1); + const auto rhs = arg(2); + if (lhs && rhs) return std::max(*lhs, *rhs); + return lhs ? lhs : rhs; + } + case IrOpcode::ReadFirstLane: + return arg(0); + case IrOpcode::Identity: + case IrOpcode::Phi: { + Bound bound; + for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { + if (const auto incoming = arg(index)) bound = std::max(bound.value_or(0u), *incoming); + } + return bound; + } + case IrOpcode::LoadVectorWindow: { + const IrValue* index = value.Argument(0)->Resolve(); + if (!index->HasImmediate()) return std::nullopt; + Bound bound = 0u; + for (const IrValue* stored : reachingStores(value, index->ImmediateU32())) { + bound = maxOf({bound, Of(*stored)}); + if (!bound) return std::nullopt; + } + return bound; + } + default: + return std::nullopt; + } + } + + std::unordered_map memo; + std::unordered_set active; +}; + +std::uint32_t IndexedWindowSlots(const IrProgram& program) { + const auto window = program.Metadata().vectorWindowSize; + WindowIndexBounds bounds; + std::uint64_t reached = 0; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::LoadVectorWindow && inst->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = inst->Argument(0)->Resolve(); + if (index->HasImmediate()) continue; + const auto bound = bounds.Of(*index); + if (bound && *bound < window / 2u) reached = std::max(reached, *bound + 1u); + } + } + return static_cast(reached); +} + +} + void DefineModule(SpirvEmitterState& state) { state.interfaceVariables.reserve(state.program.Info().inputs.size() + state.program.Info().outputs.size()); DefineInputs(state); @@ -55,9 +224,14 @@ void DefineModule(SpirvEmitterState& state) { state.scratchVariable.at(half) = state.module.AllocateId(); } } - if (state.program.Metadata().vectorWindowSize != 0u) { + if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + static const bool narrowWindow = std::getenv("APS5_NO_WINDOW_NARROWING") == nullptr; + state.vectorWindowIndexed = narrowWindow ? IndexedWindowSlots(state.program) : 0u; for (std::uint32_t half = 0; half < state.laneCount; half++) { state.vectorWindowVariable.at(half) = state.module.AllocateId(); + if (state.vectorWindowIndexed == 0u) continue; + auto& slots = state.vectorWindowSlots.at(half); + for (std::uint32_t slot = state.vectorWindowIndexed; slot < window; slot++) slots.push_back(state.module.AllocateId()); } } state.mainFunc = state.module.AllocateId(); @@ -66,7 +240,7 @@ void DefineModule(SpirvEmitterState& state) { state.meshGuestFunc = state.module.AllocateId(); state.module.EmitCapability(spv::CapabilityMeshShadingEXT); state.module.EmitExtension("SPV_EXT_mesh_shader"); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputTrianglesEXT); + state.module.AddExecutionMode(state.mainFunc, mesh.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, mesh.maxVertices); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputPrimitivesEXT, mesh.maxPrimitives); } @@ -169,6 +343,11 @@ void DefineModule(SpirvEmitterState& state) { std::uint32_t localX = workgroup->threadsNum[0] != 0u ? workgroup->threadsNum[0] : derivativeDefault; std::uint32_t localY = workgroup->threadsNum[1] != 0u ? workgroup->threadsNum[1] : derivativeDefault; std::uint32_t localZ = workgroup->threadsNum[2] != 0u ? workgroup->threadsNum[2] : 1u; + if (state.spannedWave) { + localX = 64u; + localY = 1u; + localZ = 1u; + } if (state.laneCount == 2) { localX = ((localX * localY * localZ + 63u) / 64u) * 32u; localY = 1u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 6efac49c44..055881c70e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -26,6 +26,59 @@ namespace ShaderRecompiler return value; } + std::uint32_t SpannedLane(SpirvEmitterState& state) { + return EmitBinaryU32(state, spv::OpBitwiseAnd, EmitLocalInvocationIndex(state), ConstantU32(state, 63u)); + } + + std::uint32_t SpannedSlot(SpirvEmitterState& state, std::uint32_t index) { + if (state.spannedExchangeVariable == 0u) { + state.spannedExchangeVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, 64u), spv::StorageClassWorkgroup); + state.module.AddName(state.spannedExchangeVariable, "wave_exchange"); + } + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeU32ElementPointer(state, spv::StorageClassWorkgroup), pointer, state.spannedExchangeVariable, index); + return pointer; + } + + void SpannedBarrier(SpirvEmitterState& state) { + state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask)); + } + + std::uint32_t SpannedLoad(SpirvEmitterState& state, std::uint32_t index) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, SpannedSlot(state, index)); + return value; + } + + void SpannedBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate) { + const auto lane = SpannedLane(state); + SpannedBarrier(state); + state.module.AddFunction(spv::OpAtomicStore, SpannedSlot(state, EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 1u))), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, 0u)); + SpannedBarrier(state); + const auto bit = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), ConstantU32(state, 1u), EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 31u))); + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicOr, TypeU32(state), previous, SpannedSlot(state, EmitBinaryU32(state, spv::OpShiftRightLogical, lane, ConstantU32(state, 5u))), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), Select(state, TypeU32(state), predicate, bit, ConstantU32(state, 0u))); + SpannedBarrier(state); + const auto low = SpannedLoad(state, ConstantU32(state, 0u)); + const auto high = SpannedLoad(state, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), result, low, high, ConstantU32(state, 0u), ConstantU32(state, 0u)); + } + + void SpannedShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { + const bool boolean = type == TypeBool(state); + if (!boolean && type != TypeU32(state) && type != TypeF32(state)) throw SpannedWaveUnsupported("a spanned wave shuffles only 32-bit and boolean values"); + std::uint32_t word = value; + if (boolean) word = Select(state, TypeU32(state), value, ConstantU32(state, 1u), ConstantU32(state, 0u)); + else if (type != TypeU32(state)) word = Unary(state, spv::OpBitcast, TypeU32(state), value); + SpannedBarrier(state); + state.module.AddFunction(spv::OpStore, SpannedSlot(state, SpannedLane(state)), word); + SpannedBarrier(state); + const auto read = SpannedLoad(state, EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 63u))); + if (boolean) state.module.AddFunction(spv::OpINotEqual, type, result, read, ConstantU32(state, 0u)); + else if (type != TypeU32(state)) state.module.AddFunction(spv::OpBitcast, type, result, read); + else state.module.AddFunction(spv::OpCopyObject, type, result, read); + } + } bool HostSubgroupSingleLane(const SpirvEmitterState& state) { @@ -33,6 +86,14 @@ namespace ShaderRecompiler } void EmitSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate) { + if (state.spannedWave) { + SpannedBallot(state, result, predicate); + return; + } + EmitHostSubgroupBallot(state, result, predicate); + } + + void EmitHostSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate) { if (HostSubgroupSingleLane(state)) { const auto zero = ConstantU32(state, 0u); const auto own = Select(state, TypeU32(state), predicate, ConstantU32(state, 1u), zero); @@ -43,6 +104,19 @@ namespace ShaderRecompiler } void EmitSubgroupBallotFindLsb(SpirvEmitterState& state, std::uint32_t result, std::uint32_t ballot) { + if (state.spannedWave) { + const auto low = state.module.AllocateId(); + const auto high = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1u); + const auto lowFirst = state.module.AllocateId(); + const auto highFirst = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), lowFirst, GlslStd450(state), GLSLstd450FindILsb, low); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), highFirst, GlslStd450(state), GLSLstd450FindILsb, high); + const auto lowActive = Binary(state, spv::OpINotEqual, TypeBool(state), low, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpSelect, TypeU32(state), result, lowActive, lowFirst, EmitAddU32(state, highFirst, ConstantU32(state, 32u))); + return; + } if (HostSubgroupSingleLane(state)) { const auto own = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), own, ballot, 0u); @@ -53,6 +127,10 @@ namespace ShaderRecompiler } void EmitSubgroupShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { + if (state.spannedWave) { + SpannedShuffle(state, type, result, value, lane); + return; + } if (HostSubgroupSingleLane(state)) { state.module.AddFunction(spv::OpCopyObject, type, result, value); return; @@ -60,6 +138,14 @@ namespace ShaderRecompiler state.module.AddFunction(spv::OpGroupNonUniformShuffle, type, result, ConstantU32(state, spv::ScopeSubgroup), value, lane); } + void EmitSubgroupShuffleInHalf(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { + if (!state.spannedWave) { + EmitSubgroupShuffle(state, type, result, value, EmitHostSubgroupLane(state, lane)); + return; + } + state.module.AddFunction(spv::OpGroupNonUniformShuffle, type, result, ConstantU32(state, spv::ScopeSubgroup), value, EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 31u))); + } + void EmitSubgroupElect(SpirvEmitterState& state, std::uint32_t result) { if (HostSubgroupSingleLane(state)) { state.module.AddFunction(spv::OpCopyObject, TypeBool(state), result, ConstantBool(state, true)); @@ -69,12 +155,14 @@ namespace ShaderRecompiler } std::uint32_t EmitSubgroupLocalInvocationId(SpirvEmitterState& state) { + if (state.spannedWave) return SpannedLane(state); const auto value = HostInvocationId(state); if (state.splitSubgroup) return EmitBinaryU32(state, spv::OpBitwiseAnd, value, ConstantU32(state, 31u)); return state.laneHalf == 0 ? value : EmitAddU32(state, value, ConstantU32(state, 32)); } std::uint32_t EmitHostSubgroupLane(SpirvEmitterState& state, std::uint32_t lane) { + if (state.spannedWave) return lane; if (!state.splitSubgroup) return lane; const auto base = EmitBinaryU32(state, spv::OpBitwiseAnd, HostInvocationId(state), ConstantU32(state, ~31u)); return EmitAddU32(state, base, lane); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index edabae4d45..1f76ed7314 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -51,7 +51,7 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::LayerExported) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::LayerExported, 0u); } - if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { + if ((state.laneCount == 2 || state.spannedWave) && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { throw std::runtime_error("workgroup input info is missing for a local/global invocation builtin"); @@ -767,11 +767,11 @@ void DefineMeshOutputs(SpirvEmitterState& state) { } state.meshAllocation = MeshArray(state, spv::StorageClassWorkgroup, TypeU32(state), 2u); state.meshPrimitiveData = MeshArray(state, spv::StorageClassPrivate, TypeU32(state), state.laneCount); - state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, 3u), mesh.maxPrimitives); + state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), mesh.maxPrimitives); state.meshCull = MeshArray(state, spv::StorageClassOutput, TypeBool(state), mesh.maxPrimitives); state.interfaceVariables.push_back(state.meshPrimitives); state.interfaceVariables.push_back(state.meshCull); - state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, spv::BuiltInPrimitiveTriangleIndicesEXT); + state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, mesh.outputPrimitiveSize == 2u ? spv::BuiltInPrimitiveLineIndicesEXT : spv::BuiltInPrimitiveTriangleIndicesEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationBuiltIn, spv::BuiltInCullPrimitiveEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationPerPrimitiveEXT); if (state.numWorkgroupsVariable == 0u) { @@ -837,16 +837,16 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { EmitIfCondition(state, isPrimitive, [&]() { const auto packed = MeshLoad(state, state.meshPrimitiveData, spv::StorageClassPrivate, TypeU32(state), ConstantU32(state, half)); std::array vertex {}; - for (std::uint32_t component = 0; component < 3u; component++) { + for (std::uint32_t component = 0; component < mesh.outputPrimitiveSize; component++) { const auto field = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), field, packed, ConstantU32(state, component * 10u), ConstantU32(state, 9u)); vertex.at(component) = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), vertex.at(component), GlslStd450(state), GLSLstd450UMin, field, lastVertex); } - const auto triangle = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3u), triangle, vertex[0], vertex[1], vertex[2]); - const auto trianglePointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32Vector(state, 3u), index); - state.module.AddFunction(spv::OpStore, trianglePointer, triangle); + const auto indices = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, mesh.outputPrimitiveSize), indices, std::span(vertex.data(), mesh.outputPrimitiveSize)); + const auto indicesPointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), index); + state.module.AddFunction(spv::OpStore, indicesPointer, indices); const auto nullBit = EmitBinaryU32(state, spv::OpBitwiseAnd, packed, ConstantU32(state, 0x80000000u)); const auto culled = state.module.AllocateId(); state.module.AddFunction(spv::OpINotEqual, TypeBool(state), culled, nullBit, ConstantU32(state, 0u)); @@ -1154,10 +1154,14 @@ void EmitProgram(SpirvEmitterState& state) { } } if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { - const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, window)); + const auto indexed = state.vectorWindowIndexed != 0u ? state.vectorWindowIndexed : window; + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, indexed)); const auto zero = state.module.Constant(spv::OpConstantNull, array); for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, array), state.vectorWindowVariable.at(half), spv::StorageClassFunction, zero); + for (const auto slot : state.vectorWindowSlots.at(half)) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), slot, spv::StorageClassFunction, ConstantU32(state, 0u)); + } } } if (state.pixelValidMaskVariable != 0u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index 89702e7b2b..856f8bceee 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -10,6 +10,8 @@ namespace ShaderRecompiler { namespace { +constexpr std::uint32_t ScalarReplacementLimit = 64; + bool HasLimitedUseTypes(std::span spirv) { if (spirv.size() < 5) throw std::runtime_error("SPIRV-Tools: incomplete module header"); bool int8 = false, int16 = false, float16 = false; @@ -116,7 +118,7 @@ std::vector ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::spandx10Clamp; } bool fp16Overflow() const { return floatMode.has_value() && floatMode->fp16Overflow; } + bool nativeFloat16Mode() const { return nativeFloat16 && (!floatMode.has_value() || (floatMode->floatMode & 0xccu) == 0xc0u); } IrF32 clampF16Overflow(IrF32 value, std::initializer_list sources); void rejectHalfOrDoubleOutputModifier(const RdnaOperand& operand) const; IrU32 clampF16Bits(const RdnaOperand& operand, IrU32 bits); @@ -73,7 +74,9 @@ private: IrU32 flushF16Input(IrU32 bits); IrU32 flushF16Result(IrU32 bits); IrF32 flushF16InputValue(IrF32 value); + IrF32 flushTinyF16Value(IrF32 value, std::uint32_t threshold); IrU32 convertF32ToF16Bits(IrF32 value, std::initializer_list sources); + IrU32 flushTinyF16Conversion(IrF32 value, IrU32 bits); void writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); IrU32 readU32(const RdnaOperand& operand); IrU32 flushF32Denormal(IrU32 bits); @@ -206,6 +209,7 @@ private: bool vDivFixupF16(const RdnaInstruction& inst); bool float16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse); bool float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix); + bool float16Native(const RdnaInstruction& inst, IrOpcode opcode, const RdnaOperand& lhsOperand, const RdnaOperand& rhsOperand); IrF32 fmaF16RoundedToOdd(IrF32 lhs, IrF32 rhs, IrF32 addend); bool floatUnary(const RdnaInstruction& inst, IrOpcode opcode); bool floatBinary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse); @@ -247,6 +251,9 @@ private: bool floatCube(const RdnaInstruction& inst, std::uint32_t resultKind); bool integer16Shift(const RdnaInstruction& inst, IrOpcode opcode, bool arithmetic); bool integer16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool sign); + IrU32 readU16Factor(const RdnaOperand& operand, bool highHalf, bool negate); + bool vMulLoU16(const RdnaInstruction& inst); + bool vPkMulLoU16(const RdnaInstruction& inst); bool integer16Mad(const RdnaInstruction& inst, bool sign, bool wide); IrU32 saturateU16Result(const RdnaInstruction& inst, const IrU32& value, bool sign); IrU32 wideAdd(const RdnaInstruction& inst, const IrU32& low, const IrU32& high, const IrU32& addend, bool sign); diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index 44588aa9f7..f06b151231 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -99,9 +99,6 @@ void TranslationContext::vInterpMovF32(const RdnaInstruction& inst) { } IrF32 TranslationContext::interpolationParameterF16(const RdnaInstruction& inst, std::uint32_t mode) { - if (!fragmentShaderBarycentricEnabled) { - throw std::runtime_error("16-bit interpolation of pixel input " + std::to_string(inst.source1.value) + " requires fragmentShaderBarycentric"); - } if (pixelInput == nullptr || !pixelInput->InputIsFp16(inst.source1.value)) { throw std::runtime_error("pixel input " + std::to_string(inst.source1.value) + " is read with 16-bit interpolation without FP16_INTERP_MODE"); } diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index 50bdf556cc..9520a70caf 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -115,8 +115,8 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode IrValue* lhs = nullptr; IrValue* rhs = nullptr; if (half) { - lhs = &readF16AsF32(sourceAt(inst, 0u)).Value(); - rhs = &readF16AsF32(sourceAt(inst, 1u)).Value(); + lhs = &flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))).Value(); + rhs = &flushF16InputValue(readF16AsF32(sourceAt(inst, 1u))).Value(); } else { IrValue* lhsBits = readOperand(sourceAt(inst, 0u), IrType::U32); IrValue* rhsBits = readOperand(sourceAt(inst, 1u), IrType::U32); diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index 6b76cb8f98..d14b698c77 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -198,7 +198,12 @@ void TranslationContext::vCvtOffF32I4(const RdnaInstruction& inst) { void TranslationContext::vCvtPkrtzF16F32(const RdnaInstruction& inst) { const IrF32 lhs(*readOperand(sourceAt(inst, 0u), IrType::F32)); const IrF32 rhs(*readOperand(sourceAt(inst, 1u), IrType::F32)); - const IrU32 result(ir.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&lhs.Value(), &rhs.Value()})); + IrU32 result(ir.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&lhs.Value(), &rhs.Value()})); + if (floatMode.has_value() && (floatMode->floatMode & 0x80u) == 0u) { + const IrU32 low = flushF16Result(IrU32(ir.BitwiseAnd(result.Value(), ir.Constant(0xffffu)))); + const IrU32 high = flushF16Result(IrU32(ir.ShiftRightLogical(result.Value(), ir.Constant(16u)))); + result = packU16Lanes(low, high); + } writeOperand(inst.destination, &result.Value()); } @@ -229,7 +234,7 @@ void TranslationContext::vCvtPkU8F32(const RdnaInstruction& inst) { void TranslationContext::vPackB32F16(const RdnaInstruction& inst) { const auto half = [&](std::uint32_t index) { - const IrU32 bits = readF16Bits(sourceAt(inst, index)); + const IrU32 bits = flushF16Input(readF16Bits(sourceAt(inst, index))); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); return IrU32(ir.Select(nan.Value(), quietNan16(bits).Value(), bits.Value())); }; diff --git a/core/shader/recompiler/Translation/src/Float64Instructions.cpp b/core/shader/recompiler/Translation/src/Float64Instructions.cpp index 00a0bac5b3..2f355fe3bc 100644 --- a/core/shader/recompiler/Translation/src/Float64Instructions.cpp +++ b/core/shader/recompiler/Translation/src/Float64Instructions.cpp @@ -149,9 +149,14 @@ bool TranslationContext::vDivFmasF64(const RdnaInstruction& inst) { } bool TranslationContext::vDivFixupF64(const RdnaInstruction& inst) { + const auto flush = [&](const std::array& word) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return word; + const IrU1 tiny(ir.IEqual(ir.BitwiseAnd(word[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + return std::array{IrU32(ir.Select(tiny.Value(), ir.Constant(0u), word[0].Value())), IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(word[1].Value(), ir.Constant(0x80000000u)), word[1].Value()))}; + }; const auto quotient = readF64Bits(sourceAt(inst, 0u)); - const auto denominator = readF64Bits(sourceAt(inst, 1u)); - const auto numerator = readF64Bits(sourceAt(inst, 2u)); + const auto denominator = flush(readF64Bits(sourceAt(inst, 1u))); + const auto numerator = flush(readF64Bits(sourceAt(inst, 2u))); const auto magnitude = [&](IrU32 high) { return IrU32(ir.BitwiseAnd(high.Value(), ir.Constant(0x7fffffffu))); }; const auto lowNonzero = [&](const std::array& word) { return IrU1(ir.INotEqual(word[0].Value(), ir.Constant(0u))); }; const auto isNan = [&](const std::array& word) { diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 8b0ea3b902..61948b2be5 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -11,25 +11,33 @@ void TranslateFloatInstruction(IrBuilder& builder, const RdnaInstruction& instru } bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool quietSnan) { - if (floatMode.has_value() && (floatMode->floatMode & 0xccu) != 0xc0u) { - throw std::runtime_error("packed f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept or rounding not to nearest even) is not implemented"); + if (floatMode.has_value() && (floatMode->floatMode & 0x0cu) != 0u) { + throw std::runtime_error("packed f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 rounding not to nearest even) is not implemented"); } + const bool flushResults = floatMode.has_value() && (floatMode->floatMode & 0x80u) == 0u; + const auto flushTinyResult = [&](IrF32 value) { + if (!flushResults) return value; + const IrU32 bits(ir.BitCastU32(value.Value())); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(opcode == IrOpcode::FPAdd32 ? 0x38800000u : 0x387ff000u))); + return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); + }; + const auto readLane = [&](const RdnaOperand& operand, bool high) { return flushF16InputValue(readF16LaneAsF32(operand, high, true)); }; const auto translateLane = [&](bool high) -> IrF32 { - const IrF32 lhs = readF16LaneAsF32(sourceAt(inst, 0u), high, true); - const IrF32 rhs = readF16LaneAsF32(sourceAt(inst, 1u), high, true); + const IrF32 lhs = readLane(sourceAt(inst, 0u), high); + const IrF32 rhs = readLane(sourceAt(inst, 1u), high); const auto ternary = [&](const IrF32& third) -> IrF32 { - if (opcode == IrOpcode::FPFma32) return fmaF16RoundedToOdd(lhs, rhs, third); + if (opcode == IrOpcode::FPFma32) return flushTinyResult(fmaF16RoundedToOdd(lhs, rhs, third)); return IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value(), &third.Value()})); }; if (accumulator) { - const IrF32 acc = readF16LaneAsF32(accumulatorOperand(inst), high, true); + const IrF32 acc = readLane(accumulatorOperand(inst), high); return applyF32ResultModifiers(inst.destination, clampF16Overflow(ternary(acc), {&lhs.Value(), &rhs.Value(), &acc.Value()})); } if (inst.sourceCount == 3u) { - const IrF32 third = readF16LaneAsF32(sourceAt(inst, 2u), high, true); + const IrF32 third = readLane(sourceAt(inst, 2u), high); return applyF32ResultModifiers(inst.destination, clampF16Overflow(ternary(third), {&lhs.Value(), &rhs.Value(), &third.Value()})); } - return applyF32ResultModifiers(inst.destination, clampF16Overflow(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()})), {&lhs.Value(), &rhs.Value()})); + return applyF32ResultModifiers(inst.destination, clampF16Overflow(flushTinyResult(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()}))), {&lhs.Value(), &rhs.Value()})); }; const auto laneBits = [&](const RdnaOperand& operand, bool high) { const IrU32 source = readF16SourceBits(operand); @@ -37,7 +45,7 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc IrU32 bits(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&source.Value(), &ir.Constant(selectHigh ? 16u : 0u), &ir.Constant(16u)})); if (operand.absolute) bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu))); if (high ? operand.negateHi : operand.negate) bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); - return bits; + return flushF16Input(bits); }; std::array, 3> sourceBits{}; std::array, 3> sourceBitsReady{}; @@ -51,7 +59,7 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc return sourceBits[index][lane]; }; const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, sourceLaneBits(0u, high), sourceLaneBits(1u, high)); }; - const bool native = nativeFloat16 && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); + const bool native = nativeFloat16 && !flushResults && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); const auto nativeResult = [&]() { const IrU32 lhs = packU16Lanes(sourceLaneBits(0u, false), sourceLaneBits(0u, true)); const IrU32 rhs = packU16Lanes(sourceLaneBits(1u, false), sourceLaneBits(1u, true)); @@ -213,6 +221,9 @@ bool TranslationContext::vDivFixupF16(const RdnaInstruction& inst) { if (operand.negate) { bits[index] = IrU32(ir.BitwiseXor(bits[index].Value(), ir.Constant(0x8000u))); } + if (index != 0u) { + bits[index] = flushF16Input(bits[index]); + } magnitude[index] = IrU32(ir.BitwiseAnd(bits[index].Value(), ir.Constant(0x7fffu))); } const auto isNan = [&](std::uint32_t index) { return IrU1(ir.UGreaterThan(magnitude[index].Value(), ir.Constant(0x7c00u))); }; @@ -238,20 +249,16 @@ bool TranslationContext::vDivFixupF16(const RdnaInstruction& inst) { } bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse) { + if (nativeFloat16Mode()) return float16Native(inst, opcode, sourceAt(inst, reverse ? 1u : 0u), sourceAt(inst, reverse ? 0u : 1u)); const bool measured = (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPSub32 || opcode == IrOpcode::FPMul32) && floatMode.has_value() && (floatMode->floatMode & 0x0cu) == 0u; - const auto flush = [&](IrF32 value, std::uint32_t threshold) { - const IrU32 bits(ir.BitCastU32(value.Value())); - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(threshold))); - return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); - }; const auto read = [&](const RdnaOperand& operand) { const IrF32 value = readF16AsF32(operand); - return measured && (floatMode->floatMode & 0x40u) == 0u ? flush(value, 0x38800000u) : value; + return measured ? flushF16InputValue(value) : value; }; const IrF32 lhs = read(sourceAt(inst, reverse ? 1u : 0u)); const IrF32 rhs = read(sourceAt(inst, reverse ? 0u : 1u)); IrF32 result(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()})); - if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flush(result, opcode == IrOpcode::FPMul32 ? 0x387ff000u : 0x38800000u); + if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flushTinyF16Value(result, opcode == IrOpcode::FPMul32 ? 0x387ff000u : 0x38800000u); writeF16(inst.destination, result, {&lhs.Value(), &rhs.Value()}); return true; } @@ -260,13 +267,21 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op if (mix && floatMode.has_value() && (floatMode->floatMode & 0xc0u) != 0xc0u) { throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept) is not implemented"); } + const bool measured = opcode == IrOpcode::FPFma32 && !mix && floatMode.has_value() && (floatMode->floatMode & 0x0cu) == 0u; std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); - args[index] = mix ? &readMixF32(operand).Value() : &readF16AsF32(operand).Value(); + if (mix) { + args[index] = &readMixF32(operand).Value(); + continue; + } + const IrF32 value = readF16AsF32(operand); + args[index] = measured ? &flushF16InputValue(value).Value() : &value.Value(); } if (opcode == IrOpcode::FPFma32 && !mix) { - writeF16(inst.destination, fmaF16RoundedToOdd(IrF32(*args[0]), IrF32(*args[1]), IrF32(*args[2])), {args[0], args[1], args[2]}); + IrF32 result = fmaF16RoundedToOdd(IrF32(*args[0]), IrF32(*args[1]), IrF32(*args[2])); + if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flushTinyF16Value(result, 0x387ff000u); + writeF16(inst.destination, result, {args[0], args[1], args[2]}); return true; } if (mix) { @@ -278,6 +293,25 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op return true; } +bool TranslationContext::float16Native(const RdnaInstruction& inst, IrOpcode opcode, const RdnaOperand& lhsOperand, const RdnaOperand& rhsOperand) { + const IrU32 lhs = readF16Bits(lhsOperand); + const IrU32 rhs = readF16Bits(rhsOperand); + IrU32 result; + if (opcode == IrOpcode::FPMul32) { + result = IrU32(ir.Emit(IrOpcode::FPMul16x2, IrType::U32, {&lhs.Value(), &rhs.Value()})); + } else { + const IrU32 addend = opcode == IrOpcode::FPSub32 ? IrU32(ir.BitwiseXor(rhs.Value(), ir.Constant(0x8000u))) : rhs; + result = IrU32(ir.Emit(IrOpcode::FPAdd16x2, IrType::U32, {&lhs.Value(), &addend.Value()})); + } + if (fp16Overflow()) { + const auto infinite = [&](const IrU32& value) { return IrU1(ir.IEqual(ir.BitwiseAnd(value.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); }; + const IrU1 overflow(ir.LogicalAnd(infinite(result).Value(), ir.LogicalNot(ir.LogicalOr(infinite(lhs).Value(), infinite(rhs).Value())))); + result = IrU32(ir.Select(overflow.Value(), ir.BitwiseOr(ir.BitwiseAnd(result.Value(), ir.Constant(0x8000u)), ir.Constant(0x7bffu)), result.Value())); + } + write16Bits(inst.destination, clampF16Bits(inst.destination, result)); + return true; +} + IrF32 TranslationContext::fmaF16RoundedToOdd(IrF32 lhs, IrF32 rhs, IrF32 addend) { IrValue& product = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&lhs.Value(), &rhs.Value()}); IrValue& sum = ir.Emit(IrOpcode::FPAdd32, IrType::F32, {&product, &addend.Value()}); @@ -400,8 +434,8 @@ bool TranslationContext::vCvtNormF16(const RdnaInstruction& inst, bool signedVal } bool TranslationContext::vCvtPknormF16(const RdnaInstruction& inst, bool signedValue) { - const IrU32 low = normF16(readF16Bits(sourceAt(inst, 0u)), signedValue); - const IrU32 high = normF16(readF16Bits(sourceAt(inst, 1u)), signedValue); + const IrU32 low = normF16(flushF16Input(readF16Bits(sourceAt(inst, 0u))), signedValue); + const IrU32 high = normF16(flushF16Input(readF16Bits(sourceAt(inst, 1u))), signedValue); const IrU32 result(ir.BitwiseOr(low.Value(), ir.ShiftLeftLogical(high.Value(), ir.Constant(16u)))); writeOperand(inst.destination, &result.Value()); return true; @@ -745,10 +779,7 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { if (roundMode == 1u) overflowBits = &ir.Select(*positive, ir.Constant(0x7f800000u), ir.Constant(0xff7fffffu)); else if (roundMode == 2u) overflowBits = &ir.Select(*positive, ir.Constant(0x7f7fffffu), ir.Constant(0xff800000u)); else if (roundMode == 3u) overflowBits = &ir.BitwiseOr(sign.Value(), ir.Constant(0x7f7fffffu)); - if (denorm32 == 1u || denorm32 == 2u) { - throw std::runtime_error("f32 denormal mode " + std::to_string(denorm32) + " at pc " + std::to_string(inst.programCounter) + " is not implemented"); - } - if (denorm32 == 3u) { + if (denorm32 != 0u) { const IrU32 mantissa(ir.BitwiseAnd(bits.Value(), ir.Constant(0x007fffffu))); const IrU1 isSubnormal(ir.IEqual(exponent.Value(), ir.Constant(0u))); const IrU32 sig(ir.Select(isSubnormal.Value(), mantissa.Value(), ir.BitwiseOr(mantissa.Value(), ir.Constant(0x00800000u)))); @@ -773,9 +804,9 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { const IrU32 magnitude(roundMode == 3u ? unrounded : IrU32(ir.Select(*roundUp, ir.IAdd(unrounded.Value(), ir.Constant(1u)), unrounded.Value()))); const IrU32 subResult(ir.BitwiseOr(sign.Value(), magnitude.Value())); IrU32 result(ir.Select(ir.Emit(IrOpcode::SGreaterThanEqual32, IrType::U1, {&expNorm.Value(), &ir.Constant(255u)}), *overflowBits, normResult.Value())); - result = IrU32(ir.Select(ir.Emit(IrOpcode::SLessThanEqual32, IrType::U1, {&expNorm.Value(), &ir.Constant(0u)}), subResult.Value(), result.Value())); - const IrU1 zero(ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0u))); - result = IrU32(ir.Select(zero.Value(), bits.Value(), result.Value())); + result = IrU32(ir.Select(ir.Emit(IrOpcode::SLessThanEqual32, IrType::U1, {&expNorm.Value(), &ir.Constant(0u)}), (denorm32 & 2u) != 0u ? subResult.Value() : sign.Value(), result.Value())); + const IrU1 zero((denorm32 & 1u) != 0u ? ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0u)) : ir.IEqual(exponent.Value(), ir.Constant(0u))); + result = IrU32(ir.Select(zero.Value(), sign.Value(), result.Value())); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); result = IrU32(ir.Select(ir.IEqual(exponent.Value(), ir.Constant(0xffu)), ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()), result.Value())); writeOperand(inst.destination, &ir.BitCastF32(result.Value())); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index d1af902f6c..353a6b2fc3 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -25,6 +25,36 @@ bool TranslationContext::integer16Binary(const RdnaInstruction& inst, IrOpcode o return true; } +IrU32 TranslationContext::readU16Factor(const RdnaOperand& operand, bool highHalf, bool negate) { + const IrU32 raw = applyBitSourceModifiers(operand, readRawU32(operand)); + const IrU32 field(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&raw.Value(), &ir.Constant(highHalf ? 16u : 0u), &ir.Constant(highHalf ? 16u : 24u)})); + return negate ? IrU32(ir.BitwiseXor(field.Value(), ir.Constant(0x8000u))) : field; +} + +bool TranslationContext::vMulLoU16(const RdnaInstruction& inst) { + const RdnaOperand& lhs = sourceAt(inst, 0u); + const RdnaOperand& rhs = sourceAt(inst, 1u); + const IrU32 left = readU16Factor(lhs, lhs.opSel, false); + const IrU32 right = readU16Factor(rhs, rhs.opSel, false); + write16Bits(inst.destination, IrU32(ir.BitwiseAnd(ir.IMul(left.Value(), right.Value()), ir.Constant(0xffffu)))); + return true; +} + +bool TranslationContext::vPkMulLoU16(const RdnaInstruction& inst) { + const RdnaOperand& lhs = sourceAt(inst, 0u); + const RdnaOperand& rhs = sourceAt(inst, 1u); + const auto lane = [&](bool highLane) { + const IrU32 left = readU16Factor(lhs, highLane ? lhs.opSelHi : lhs.opSel, highLane ? lhs.negateHi : lhs.negate); + const IrU32 right = readU16Factor(rhs, highLane ? rhs.opSelHi : rhs.opSel, highLane ? rhs.negateHi : rhs.negate); + return IrU32(ir.IMul(left.Value(), right.Value())); + }; + const IrU32 low = lane(false); + const IrU32 high = lane(true); + const IrU32 result = packU16Lanes(low, high); + writeOperand(inst.destination, &result.Value()); + return true; +} + IrU32 TranslationContext::saturateU16Result(const RdnaInstruction& inst, const IrU32& value, bool sign) { if (!inst.destination.clamp) { return value; diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 5de4543f7b..992dcf3388 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -217,7 +217,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.maxPrimitives = mesh->maxPrimitives; target.provokingVertex = mesh->provokingVertex; target.esgsItemSize = mesh->esgsItemSize; - if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || target.provokingVertex > 2u) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); + target.outputPrimitiveSize = mesh->outputPrimitiveSize; + if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || (target.outputPrimitiveSize != 2u && target.outputPrimitiveSize != 3u) || target.provokingVertex >= target.outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); } ShaderStageInputInfo result; result.vertex = &vertexStorage; diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index cf6c3a789f..a199f1ff65 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -117,7 +117,7 @@ IrF32 TranslationContext::readMixF32(const RdnaOperand& operand) { if (!operand.opSelHi) { return IrF32(*readOperand(operand, IrType::F32)); } - return readF16LaneAsF32(operand, operand.opSel, false); + return flushF16InputValue(readF16LaneAsF32(operand, operand.opSel, false)); } IrU32 TranslationContext::readF16LaneBits(const RdnaOperand& operand, bool highLane) { @@ -165,7 +165,7 @@ void TranslationContext::write16Bits(const RdnaOperand& operand, IrU32 value) { } bool TranslationContext::directedF16Rounding() const { - return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u && ((floatMode->floatMode >> 6u) & 3u) == 3u; + return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u; } IrU32 TranslationContext::flushF16Bits(IrU32 bits, std::uint32_t keepBit) { @@ -184,8 +184,12 @@ IrU32 TranslationContext::flushF16Result(IrU32 bits) { IrF32 TranslationContext::flushF16InputValue(IrF32 value) { if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return value; + return flushTinyF16Value(value, 0x38800000u); +} + +IrF32 TranslationContext::flushTinyF16Value(IrF32 value, std::uint32_t threshold) { const IrU32 bits(ir.BitCastU32(value.Value())); - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x38800000u))); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(threshold))); return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); } @@ -220,12 +224,28 @@ IrU32 TranslationContext::convertF32ToF16Bits(IrF32 value, std::initializer_list return IrU32(ir.Select(ir.ULessThan(magnitude.Value(), ir.Constant(0x7f800000u)), result.Value(), native.Value())); } +IrU32 TranslationContext::flushTinyF16Conversion(IrF32 value, IrU32 bits) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u) return bits; + const std::uint32_t rounding = (floatMode->floatMode >> 2u) & 3u; + const IrU32 word(ir.BitCastU32(value.Value())); + const IrU32 magnitude(ir.BitwiseAnd(word.Value(), ir.Constant(0x7fffffffu))); + const IrU32 sign(ir.BitwiseAnd(ir.ShiftRightLogical(word.Value(), ir.Constant(16u)), ir.Constant(0x8000u))); + IrValue* limit = &ir.Constant(rounding == 0u ? 0x387ff000u : 0x38800000u); + if (rounding == 1u || rounding == 2u) { + const IrU1 away(ir.IEqual(sign.Value(), ir.Constant(rounding == 1u ? 0u : 0x8000u))); + limit = &ir.Select(away.Value(), ir.Constant(0x387fe001u), ir.Constant(0x38800000u)); + } + const IrU1 tiny(ir.ULessThan(magnitude.Value(), *limit)); + return IrU32(ir.Select(tiny.Value(), sign.Value(), bits.Value())); +} + void TranslationContext::writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources) { - if (!directedF16Rounding()) { + if (!directedF16Rounding() && (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u)) { writeF16(operand, value, sources); return; } - write16Bits(operand, convertF32ToF16Bits(applyF16ResultModifiers(operand, value), sources)); + const IrF32 modified = applyF16ResultModifiers(operand, value); + write16Bits(operand, flushTinyF16Conversion(modified, convertF32ToF16Bits(modified, sources))); } void TranslationContext::writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources) { diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 645b98534e..db326fa784 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -75,6 +75,7 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCmpxNleF32: case RdnaOpcode::VCmpxNlgF32: case RdnaOpcode::VCmpxNltF32: + return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VCvtI32F32: case RdnaOpcode::VCvtU32F32: case RdnaOpcode::VCvtRpiI32F32: @@ -855,7 +856,7 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VPkMadU16: return packedInteger16Mad(inst, false); case RdnaOpcode::VPkMulLoU16: - return packedInteger16Binary(inst, IrOpcode::IMul32, false); + return vPkMulLoU16(inst); case RdnaOpcode::VPkAddI16: return packedInteger16Binary(inst, IrOpcode::IAdd32, true); case RdnaOpcode::VPkAddU16: @@ -1140,7 +1141,7 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VSubNcI32: return vAddSubNcI32(inst, true); case RdnaOpcode::VMulLoU16: - return integer16Binary(inst, IrOpcode::IMul32, false); + return vMulLoU16(inst); case RdnaOpcode::VMadU16: return integer16Mad(inst, false, false); case RdnaOpcode::VMadI16: diff --git a/core/shader/recompiler/UserDataUse.hpp b/core/shader/recompiler/UserDataUse.hpp new file mode 100644 index 0000000000..c193714270 --- /dev/null +++ b/core/shader/recompiler/UserDataUse.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_SHADER_RECOMPILER_USERDATAUSE_HPP +#define CORE_SHADER_RECOMPILER_USERDATAUSE_HPP + +#include +#include +#include + +namespace ShaderRecompiler { + +struct SrtUserDataUse { + static constexpr std::uint32_t TrackedWords = 64; + std::uint64_t asBase = 0; + std::uint64_t asValue = 0; + std::vector> reads; +}; + +} + +#endif diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index b8d4b0e10e..f30aae1501 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -206,11 +206,11 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 30u; + upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -245,11 +245,11 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 30u; + layered[4] = 32u; layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t), 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -270,10 +270,10 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 30u; + bytes[4] = 32u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t), 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -394,10 +394,10 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 30u; + upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; @@ -407,12 +407,12 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 30u; + main25[4] = 32u; if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } - main25.insert(main25.end(), sizeof(std::uint64_t), 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -426,9 +426,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 30u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 32u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 31u; + future[4] = 33u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -446,5 +446,10 @@ int main() { Require(rejected); } + for (const bool spanned : {false, true}) { + request.target.spannedWaves = spanned; + Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); + } + return 0; } diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 79fc9f901d..bb48086a67 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -195,8 +195,19 @@ async function selectFile(selected, files) { worker.postMessage(files ? { type: "inspect", files } : { type: "inspect", file }); } -$("choose").onclick = () => $("file").click(); -$("change").onclick = () => { $("drop").hidden = false; }; +$("choose").onclick = () => { $("file").value = ""; $("file").click(); }; +$("change").onclick = () => { + if (working) return; + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + file = null; + summary = null; + $("file").value = ""; + $("folder").value = ""; + for (const id of ["selection", "warnings", "error", "done", "download"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("drop").hidden = false; + update(); +}; $("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; $("folder").onchange = () => { const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index f1a30d2e42..eca9ca9778 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -27,24 +27,45 @@ function safeSize(value) { return value; } -async function headerBytes(entry) { - if (entry.uncompressedSize < 4) return null; +async function headerBytes(entry, size = 4) { + if (entry.uncompressedSize < size) return null; const stop = new AbortController(); - const header = new Uint8Array(4); + const header = new Uint8Array(size); let filled = 0; try { await entry.getData(new WritableStream({ write(chunk) { - const length = Math.min(4 - filled, chunk.length); + const length = Math.min(size - filled, chunk.length); header.set(chunk.subarray(0, length), filled); filled += length; - if (filled === 4) stop.abort(); + if (filled === size) stop.abort(); } }), { signal: stop.signal, checkSignature: false }); } catch (error) { - if (!stop.signal.aborted || filled !== 4) throw error; + if (!stop.signal.aborted || filled !== size) throw error; } - return header; + return filled === size ? header : null; +} + +async function isAppleDouble(entry) { + const header = await headerBytes(entry, 26); + const view = header && new DataView(header.buffer, header.byteOffset, header.byteLength); + if (!view || view.getUint32(0) !== 0x00051607 || view.getUint32(4) !== 0x00020000) return false; + const size = 26 + view.getUint16(24) * 12; + const table = size === 26 ? header : await headerBytes(entry, size); + if (!table) return false; + const descriptors = new DataView(table.buffer, table.byteOffset, table.byteLength); + const identifiers = new Set(); + for (let position = 26; position < size; position += 12) { + const identifier = descriptors.getUint32(position); + const offset = descriptors.getUint32(position + 4); + const length = descriptors.getUint32(position + 8); + if (identifier === 0 || identifier === 1 || identifiers.has(identifier) + || offset > entry.uncompressedSize || length > entry.uncompressedSize - offset + || ((offset !== 0 || length !== 0) && offset < size)) return false; + identifiers.add(identifier); + } + return true; } const SELF_MAGICS = [ @@ -122,12 +143,30 @@ export class Archive { this.entries.push(entry); } if (!this.entries.some(entry => !entry.directory)) throw new Error("No files were selected"); + const directories = new Set(); 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}`); + const parent = parts.slice(0, index).join("/").toLowerCase(); + if (paths.get(parent) === false) throw new Error(`A file is also used as a folder: ${entry.filename}`); + if (!entry.filename.toLowerCase().startsWith("__macosx/")) directories.add(parent); } } + const applicationPaths = new Map([...paths].filter(([name]) => name !== "__macosx" && !name.startsWith("__macosx/"))); + const metadata = this.entries.filter(entry => entry.filename.startsWith("__MACOSX/")); + let appleDouble = metadata.some(entry => !entry.directory); + for (const entry of metadata) { + if (entry.directory) { + const companion = entry.filename.slice("__MACOSX/".length).replace(/\/$/u, "").toLowerCase(); + if (companion && applicationPaths.get(companion) !== true && !directories.has(companion)) { appleDouble = false; break; } + continue; + } + const match = /^__MACOSX\/(.*\/)?\._([^/]+)$/u.exec(entry.filename); + const companion = match && ((match[1] ?? "") + match[2]).toLowerCase(); + if (!companion || (!applicationPaths.has(companion) && !directories.has(companion)) || !await isAppleDouble(entry)) { appleDouble = false; break; } + } + if (appleDouble) this.entries = this.entries.filter(entry => !entry.filename.startsWith("__MACOSX/")); + if (!this.entries.some(entry => !entry.directory)) throw new Error("No files were selected"); let prefix = ""; const files = this.entries.filter(entry => !entry.directory); while (files.every(entry => entry.filename.slice(prefix.length).includes("/"))) { @@ -308,6 +347,21 @@ export async function packageArchive(archive, converted, outputName, libraryFile if (!module.name.startsWith("app0/sce_module/")) throw new Error("Invalid converted module path"); claim(module.name); } + const outputEntries = [ + ...libraryEntries.map(entry => ({ name: entry.filename, directory: entry.directory })), + ...originalFiles.map(item => ({ name: "app0/" + item.name, directory: item.entry.directory })), + { name: outputName }, + ...(converted.diagnostics ? [{ name: "windows-diagnostics-imports.txt" }] : []), + ...converted.modules.map(module => ({ name: module.name })) + ]; + const files = new Set(outputEntries.filter(entry => !entry.directory).map(entry => entry.name.toLowerCase())); + for (const { name } of outputEntries) { + const parts = name.replace(/\/$/u, "").toLowerCase().split("/"); + for (let index = 1; index < parts.length; index++) { + const parent = parts.slice(0, index).join("/"); + if (files.has(parent)) throw new Error(`Output path collision: ${parent}`); + } + } 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))); diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs index deba9ee898..1b305c61ad 100644 --- a/demo/web/tests/archive-root.test.mjs +++ b/demo/web/tests/archive-root.test.mjs @@ -10,10 +10,163 @@ const header = new Uint8Array([127, 69, 76, 70]); async function zipped(entries) { const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); - for (const [name, directory] of entries) await writer.add(name, directory ? undefined : new Uint8ArrayReader(header), { directory }); + for (const [name, directory, bytes = header, options = {}] of entries) await writer.add(name, directory ? undefined : new Uint8ArrayReader(bytes), { directory, ...options }); return writer.close(); } +const { packageArchive } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const libraryBundle = await zipped([["libs/provider.prx", false]]); +for (const conflict of ["sce_module/provider.guest.prx/child", "sce_module/PROVIDER.GUEST.PRX/child", "sce_module/provider.guest.prx/"]) { + const input = new Archive(await zipped([["main.elf", false], ["sce_module/provider.prx", false], [conflict, conflict.endsWith("/")]])); + try { + await input.inspect(() => {}); + const written = []; + const destination = { + async directory(name) { written.push(name); }, + async file(name) { written.push(name); return new WritableStream(); } + }; + const converted = { executable: header, modules: [{ name: "app0/sce_module/provider.guest.prx", bytes: header }] }; + await assert.rejects(packageArchive(input, converted, "main", libraryBundle, destination, () => {}, true), /Output path collision/); + assert.deepEqual(written, []); + } finally { await input.close(); } +} + +const distinctOutput = new Archive(await zipped([["main.elf", false], ["sce_module/provider.prx", false], ["assets/", true]])); +try { + await distinctOutput.inspect(() => {}); + const written = []; + await packageArchive(distinctOutput, { executable: header, modules: [{ name: "app0/sce_module/provider.guest.prx", bytes: header }] }, + "main", libraryBundle, { + async directory(name) { written.push(name); }, + async file(name) { written.push(name); return new WritableStream(); } + }, () => {}, true); + assert(written.includes("app0/assets/")); + assert(written.includes("app0/sce_module/provider.guest.prx")); + assert(written.includes("libs/provider.prx")); +} finally { await distinctOutput.close(); } + +const metadata = new Uint8Array(86); +const metadataView = new DataView(metadata.buffer); +metadataView.setUint32(0, 0x00051607); +metadataView.setUint32(4, 0x00020000); +metadataView.setUint16(24, 2); +metadataView.setUint32(26, 9); +metadataView.setUint32(30, 50); +metadataView.setUint32(34, 32); +metadataView.setUint32(38, 2); +metadataView.setUint32(42, 82); +metadataView.setUint32(46, 4); +const app = [["Title/main.elf", false], ["Title/sce_module/provider.prx", false]]; +const sidecars = [["__MACOSX/Title/._main.elf", false, metadata], ["__MACOSX/Title/sce_module/._provider.prx", false, metadata]]; +for (const directories of [[], ["Title/", "Title/sce_module/", "__MACOSX/", "__MACOSX/Title/", "__MACOSX/Title/sce_module/"]]) { + const archive = new Archive(await zipped([...directories.map(name => [name, true]), ...app, ...sidecars, ["__MACOSX/._Title", false, metadata]])); + try { + const summary = await archive.inspect(() => {}); + assert.equal(summary.root, "Title/"); + assert.equal(summary.files, 2); + assert.equal(summary.modules, 1); + assert.equal(summary.selectedBytes, 8); + assert.equal(summary.totalBytes, 8 + metadata.length * 3); + assert(!archive.files.some(item => item.name.includes("__MACOSX"))); + for (const target of ["linux", "windows"]) { + const input = await archive.conversionInput(target, false, () => {}); + assert.deepEqual(input.executable, header); + assert.equal(input.modules[0].name, "sce_module/provider.prx"); + assert.deepEqual(input.modules[0].bytes, header); + } + } finally { await archive.close(); } +} + +const wrongMagic = metadata.slice(); +wrongMagic[3] = 0; +const wrongVersion = metadata.slice(); +wrongVersion[5] = 1; +const oversizedTable = metadata.slice(); +new DataView(oversizedTable.buffer).setUint16(24, 100); +const invalidDescriptors = []; +for (const [position, value] of [ + [26, 0], [26, 1], [38, 9], [30, 0], [30, 49], + [30, 0xffffffff], [34, 0xffffffff], [42, 83], [42, 87] +]) { + const invalid = metadata.slice(); + new DataView(invalid.buffer).setUint32(position, value); + invalidDescriptors.push(invalid); +} +const emptyBeforeTable = metadata.slice(); +new DataView(emptyBeforeTable.buffer).setUint32(30, 25); +new DataView(emptyBeforeTable.buffer).setUint32(34, 0); +invalidDescriptors.push(emptyBeforeTable); +const emptyPastEnd = metadata.slice(); +new DataView(emptyPastEnd.buffer).setUint32(42, 87); +new DataView(emptyPastEnd.buffer).setUint32(46, 0); +invalidDescriptors.push(emptyPastEnd); +for (const extra of [ + [["__MACOSX/Title/._main.elf", false, wrongMagic]], + [["__MACOSX/Title/._main.elf", false, wrongVersion]], + [["__MACOSX/Title/._main.elf", false, metadata.subarray(0, 25)]], + [["__MACOSX/Title/._main.elf", false, oversizedTable]], + ...invalidDescriptors.map(bytes => [["__MACOSX/Title/._main.elf", false, bytes]]), + [["__MACOSX/Title/._absent.elf", false, metadata]], + [...sidecars, ["__MACOSX/Title/readme", false, new Uint8Array([1])]], + [...sidecars, ["__MACOSX/unrelated/", true]], + [...sidecars, ["other/resource", false, new Uint8Array([1])]] +]) { + const archive = new Archive(await zipped([...app, ...extra])); + try { await assert.rejects(archive.inspect(() => {}), /exactly one executable ELF/); } + finally { await archive.close(); } +} + +for (const [offset, length] of [[0, 0], [50, 0], [86, 0]]) { + const emptyEntry = metadata.slice(); + new DataView(emptyEntry.buffer).setUint32(30, offset); + new DataView(emptyEntry.buffer).setUint32(34, length); + const archive = new Archive(await zipped([["main.elf", false], ["__MACOSX/._main.elf", false, emptyEntry]])); + try { + assert.equal((await archive.inspect(() => {})).files, 1); + assert.deepEqual(archive.files.map(item => item.name), ["main.elf"]); + } finally { await archive.close(); } +} + +for (const bytes of [wrongMagic, wrongVersion, metadata.subarray(0, 25), oversizedTable, ...invalidDescriptors]) { + const name = "__MACOSX/._main.elf"; + const archive = new Archive(await zipped([["main.elf", false], [name, false, bytes]])); + try { + assert.equal((await archive.inspect(() => {})).files, 2); + assert.deepEqual(archive.files.map(item => item.name), ["main.elf", name]); + } finally { await archive.close(); } +} + +for (const unrelated of [ + ["__MACOSX/readme", false, new Uint8Array([1])], ["__MACOSX/unrelated/", true], + ["__MACOSX/__MACOSX/._._main.elf", false, metadata], ["__MACOSX/__MACOSX/", true] +]) { + const archive = new Archive(await zipped([["main.elf", false], ["__MACOSX/._main.elf", false, metadata], unrelated])); + try { + await archive.inspect(() => {}); + assert.deepEqual(archive.files.map(item => item.name), ["main.elf", "__MACOSX/._main.elf", unrelated[0]]); + } finally { await archive.close(); } +} + +const knownDirectory = new Archive(await zipped([ + ["main.elf", false], ["assets/", true], ["__MACOSX/._main.elf", false, metadata], ["__MACOSX/assets/", true] +])); +try { + await knownDirectory.inspect(() => {}); + assert.deepEqual(knownDirectory.files.map(item => item.name), ["main.elf", "assets/"]); +} finally { await knownDirectory.close(); } + +for (const [entries, message] of [ + [[...app, ["__MACOSX/../._main.elf", false, metadata]], /Unsafe ZIP path/], + [[...app, ...sidecars, ["__MACOSX/title/._MAIN.ELF", false, metadata]], /Duplicate ZIP path/], + [[...app, ["__MACOSX/Title/._main.elf", false, metadata, { password: "metadata-password" }]], /Encrypted ZIP entries/], + [[...app, ["__MACOSX/Title/._main.elf", false, metadata, { versionMadeBy: 0x31e, externalFileAttributes: (0o120777 << 16) >>> 0 }]], /Links and special files/], + [[["Title/", true], ["__MACOSX/._Title", false, metadata]], /No files were selected/] +]) { + const archive = new Archive(await zipped(entries)); + try { await assert.rejects(archive.inspect(() => {}), message); } + finally { await archive.close(); } +} + const parents = ["outer/", "outer/inner/", "outer/inner/title/"]; for (const directories of [[], parents, [...parents].reverse()]) { const archive = new Archive(await zipped([ @@ -28,6 +181,56 @@ for (const directories of [[], parents, [...parents].reverse()]) { } finally { await archive.close(); } } +{ + const originalFetch = globalThis.fetch; + const originalSelf = globalThis.self; + globalThis.self = { postMessage() {} }; + try { + const { downloadLibraries } = await import(pathToFileURL(path.join(process.argv[2], "worker.mjs"))); + const bytes = Uint8Array.from({ length: 32 }, (_, index) => index); + const sha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); + const asset = { path: "libraries/linux-1234.zip", size: bytes.length, expandedBytes: 100, sha256 }; + let cancelled = false; + let pulled = 0; + const configureDownload = (chunks, size = bytes.length) => { + cancelled = false; + pulled = 0; + globalThis.fetch = async url => { + if (url === "./release.json") return Response.json({ tag: "v1", platforms: { linux: { ...asset, size } } }); + if (url.startsWith("https://api.github.com/")) return Response.json({ tag_name: "v1" }); + return new Response(new ReadableStream({ + pull(controller) { + if (pulled < chunks.length) controller.enqueue(chunks[pulled++]); + else controller.close(); + }, + cancel() { cancelled = true; } + })); + }; + }; + configureDownload(Array.from({ length: 8 }, () => bytes)); + await assert.rejects(downloadLibraries("linux"), /System library download is incomplete/); + assert.equal(cancelled, true); + assert(pulled < 8); + for (const chunks of [[], [bytes.subarray(0, 31)]]) { + configureDownload(chunks); + await assert.rejects(downloadLibraries("linux"), /System library download is incomplete/); + } + configureDownload([bytes.subarray(0, 3), bytes.subarray(3)]); + const downloaded = await downloadLibraries("linux"); + assert.deepEqual(new Uint8Array(await downloaded.file.arrayBuffer()), bytes); + assert.equal(downloaded.expandedBytes, 100); + for (const size of [-1, 1.5, 512 * 1024 * 1024 + 1]) { + configureDownload([bytes], size); + await assert.rejects(downloadLibraries("linux"), /Invalid release manifest/); + assert.equal(pulled, 0); + } + } finally { + globalThis.fetch = originalFetch; + if (originalSelf === undefined) delete globalThis.self; + else globalThis.self = originalSelf; + } +} + const folder = new Archive([{ path: "outer/inner/title/main.elf", file: new Blob([header]) }]); try { assert.equal((await folder.inspect(() => {})).root, "outer/inner/title/"); } finally { await folder.close(); } diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index d2cbdb4377..fcd816ffe8 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -21,7 +21,7 @@ export async function downloadLibraries(target) { 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"); + if (!asset || !Number.isSafeInteger(asset.size) || asset.size < 0 || asset.size > 512 * 1024 * 1024 || !/^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) { const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); @@ -33,8 +33,27 @@ export async function downloadLibraries(target) { } 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 reader = download.body.getReader(); + const chunks = []; + let size = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > asset.size) throw new Error("System library download is incomplete"); + chunks.push(value); + } + if (size !== asset.size) throw new Error("System library download is incomplete"); + } catch (error) { + await reader.cancel(error).catch(() => {}); + throw error; + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } 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 }; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 8a1a764275..1d4b7f1223 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -95,11 +95,15 @@ flowchart LR - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. +- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. Explicit ABI resolution still prepares its requested stages. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. - On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - 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. @@ -107,7 +111,26 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, only for imports of 1 MiB or more (a smaller import is compared, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread); the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, only for imports of 1 MiB or more (a smaller import is compared, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread); the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. + +### Log output + +```mermaid +flowchart LR + callers["Any thread:
WriteLogLine,
AGC_LOG_DEFERRED"] --> queue["Queue, 16 MiB:
a full queue blocks the caller"] + queue --> logger["Logger thread:
format, join lines per stream,
fwrite and flush"] + logger --> streams["stdout, stderr"] + exits["Abort, terminate, crash report,
_Exit"] -- "bounded flush, 2 s" --> queue +``` + +`WriteLogLine` ([`LogMacros.hpp`](../../core/libs/prx/libc/include/general/LogMacros.hpp), behind `APS5_LOG_OUT` and `APS5_LOG_ERR`) formats a line on the calling thread and hands it to the module's `LogSink()` when one is installed; without a sink it writes and flushes it there. `Driver` installs [`AsyncLog`](../../core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp) as the sink of `libSceAgcDriver`, so the queue workers no longer wait for the console or the log file for those lines. `APS5_LOG_SYNC` (any value) skips the installation and restores the synchronous path. + +- **Queue.** One logger thread drains a queue that holds at most 16 MiB of waiting lines; the batch the logger is writing comes on top. A caller that finds the queue full blocks until the logger has taken the pending batch, so no line is dropped. The logger writes entries in queue order and joins consecutive entries of one stream; it writes them with one `fwrite` and flush when the text reaches 64 KiB, the stream changes or the batch ends. `AGC_LOG_DEFERRED` queues the format string with numeric, enum and `LogLiteral` arguments and formats them on the logger thread; `APS5_LOG_TIMESTAMPS` stamps a line when it is queued. +- **Order.** Lines that pass through the sink, on stdout and stderr alike, appear in call order. A direct `fprintf(stderr, ...)` or `printf` does not pass through it and is written at once, so it can appear before older queued lines; the coalesced `deferred write-back failed` notes are of this kind. `APS5_LOG_SYNC=1` keeps one order for all of them. +- **Flush.** `Flush` waits without a limit until every line queued before the call has been written, including while another thread holds the stream. It runs when the driver fails and in `Driver::stop`. At exit `AsyncLog` runs a bounded flush (`FlushFor`, 2 s) instead: on Windows the `atexit` handlers of a DLL run after the process has ended its other threads, so the logger thread may be gone with lines still queued, and a `Flush` there would wait forever. It also registers that bounded flush with `LibcSetEndFlush_nid_no_patch`; libc runs it where the process ends without `atexit`: `abort()` and `std::terminate` (a `SIGABRT` handler that the first registration chains in front of the previous one, so `raise(SIGABRT)` still ends the process), guest `abort()`, the Windows crash report of a fatal exception and guest `_Exit`. A title that installs its own `SIGABRT` disposition replaces that handler. The bounded flush polls two counters and takes no lock, so a thread that dies holding the queue lock or a stream cannot stall it. After 2 s it gives up, later calls return at once, and the lines still queued are lost. +- **Statistics.** A `[log]` line every 10 s in which lines were logged reports lines, bytes, the time spent by the callers and by the logger, and the queue peak; `APS5_LOG_STATS=0` hides it. Identical `deferred write-back failed` notes print three times per 10 s and are then counted in one `repeated N more times` line. + +`agc_async_log` checks the order of deferred and written lines, long lines, the repeat limit, a `Flush` that waits for a held stream, a bounded flush that gives up, and a full queue that blocks its caller. `agc_async_log_fatal` starts the test executable as a child that queues lines behind a held stream and then calls `abort()`, calls `std::terminate()`, raises an access violation (Windows) or `SIGABRT` (elsewhere), or aborts without ever releasing the stream; the first four must leave every line in the file in order, the last must still end after the bound. ### Runtime image and sampler tables @@ -136,7 +159,7 @@ The comparison runs again when any of them changes. A CPU store into unwatched m A cached shader keeps an `IrResourcePlan` (`ResourceMaterializer::ExtractPlan` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp)) for as long as it is registered: a copy of the IR values its descriptor sources, SRT reads, uniform fill and control-flow conditions depend on, which every capture walks to read the shader's resources from guest memory. The copy is sized to what the walk can use: -- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. +- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. A first-lane read is judged by its source under the read's active mask, and a constant read by the SRT read it names, as the evaluator reads them. A first-lane read that reaches itself, through a loop-carried phi or a constant slot that holds it, is not evaluable: both `MayEvaluate` and the evaluator track the first-lane reads they are already inside, since each one starts a new walk under its own mask. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. - The copied values are allocated in one `IrValueArena` and hold no use lists: the walk only follows arguments. ### Shader MODE register access diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 2dfcffda8d..03e4c72be7 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -2,7 +2,7 @@ ## Relinker only -The relinker can be built without initializing submodules or configuring SDL, Vulkan, FFmpeg, FreeType or the shader recompiler. It requires CMake, a C++20 compiler and a build tool. Python 3 enables the Python regression tests. +The relinker can be built without initializing submodules or configuring SDL, Vulkan, FFmpeg, FreeType or the shader recompiler. It requires CMake, a C++20 compiler and a build tool. Python 3.9 or newer enables the Python regression tests. ```sh cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON @@ -14,7 +14,7 @@ Python must be found during CMake configuration for Python tests to be registere This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools; Windows uses the MinGW-w64 toolchain below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). -The output remains x86-64 Linux ELF or Windows PE. Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries or provide macOS game execution. Tests inspect both output formats; execution checks run only on their compatible hosts. +The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. Use a separate build directory for the full build. @@ -32,6 +32,7 @@ git submodule update --init --recursive - 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`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. +- Bitdefender Advanced Threat Defense hooks Windows calls that `sceKernelRaiseException` makes while its target thread is suspended. When the target is suspended inside ntdll's exception dispatch while it holds an ntdll lock, the hook waits for that lock and `guest_raise_exception` hangs until its timeout. A scanning exception for the build directory does not prevent it; add `guest_raise_exception_tests.exe` to the Advanced Threat Defense exceptions. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. ## Commands @@ -59,6 +60,7 @@ Project switches accept `ON` or `OFF`: |----------------------------------|---------|--------------------------------------------------| | `-DBUILD_TESTING=ON` | `OFF` | Build and register tests. | | `-DANYPS5_RELINKER_ONLY=ON` | `OFF` | Build only the relinker and its tests, without third-party dependencies. | +| `-DANYPS5_BUILD_WASM=ON` | `OFF` | Build only the browser relinker (`relinker_web` target). Requires `emcmake` from Emscripten and `BUILD_TESTING=OFF`. | | `-DANYPS5_ENABLE_SPIRV_TOOLS=ON` | `OFF` | Enable SPIR-V validation and optimization. | | `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | @@ -173,6 +175,34 @@ The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The interv With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. +## GPU time per command class + +Set `APS5_PROFILE_GPU=1` (any value) to time the recorder's GPU work with timestamp queries and print one `[gputime]` line on stderr. `APS5_PROFILE_DRAW` alone does not print it: a batch then gets only its whole-batch timestamp pair. A timed range is a pair of bottom-of-pipe stamps that both wait for all earlier work, so a range is the time its own work adds after everything recorded before it. A batch holds at most 512 ranges, its whole-batch range included. The line is written when a batch's timestamps are read after its fence, at least 10 s after the previous line, and covers every batch read since then. + +The header fields: ` ms of GPU time` is the sum of the program-keyed ranges only (dispatches and buffer copies, see `by program`); draws and the driver's own commands are not in it. `in batches` counts the batches read. `batch ms first to last command` sums the whole-batch ranges, from the stamp written when the batch opens (it waits for the batches before it) to the stamp written at `Submit`. `classes ms` is the plain sum of every class range, including `present-blit`, which runs outside the batches. `all ranges ms` is, per batch, the union of its class and program ranges, with overlapping time counted once; `present-blit` is not in it. `untimed ms` is `batch` minus `all ranges`: batch time that no range covers, including the work of dropped ranges. ` presents` counts the `VulkanDevice::present` calls since the previous line. ` ranges dropped at the 512 cap` counts ranges refused because their batch was full. + +A class range covers a command together with its own barriers. In `VulkanDevice::CopyBuffer` the `copy` class range spans the leading barrier, the transfer and the trailing barrier, and the program-keyed range sits inside it around the transfer alone, so the transfer counts in both `GPU time` and `classes` but once in `all ranges`. A dispatch's leading and trailing barriers are ranges of their own classes, placed before and after its program range, not around it. `GPU time` plus `classes` can therefore exceed `batch`; compare `all ranges` with `batch` instead. + +`by program` lists up to 12 keys, highest time first, as `0x x ms`. A dispatch's key is its guest program address, or the program's `variantId` when the address is 0. A buffer copy's key is the program address of the copy kernel it replaces, or 0 for a PM4 DMA copy (opcode 0x50). `by class` lists every class with at least one range since the previous line as ` x ms MiB (per present x ms MiB)`; the per-present figures are 0 when there was no present. MiB is the byte count each range passes to `EndGpuTiming`. The classes, with the function that opens each range: + +- `dispatch-lead`: a dispatch's leading barrier, when it is recorded rather than merged into the previous trailing barrier (`VulkanDevice::recordDispatch`). 0 MiB. +- `dispatch-trail`: a dispatch's trailing barrier (`recordDispatch`). 0 MiB. +- `indirect-args`: the barrier that makes an indirect dispatch's host-imported group counts visible to the indirect read (`recordDispatch`). 12 bytes per range. +- `label-run`: a run of GPU label stores (`vkCmdUpdateBuffer`, plus the queued occlusion dump folds of a per-batch run) with its barriers (`Recorder::RecordStore` for inline runs, `Recorder::closeStoreRun` for per-batch runs). Stored bytes. +- `fill`: a guest fill of host-imported memory, as `vkCmdFillBuffer` or pattern copies, with its barriers (`VulkanDevice::FillBuffer`). Filled bytes. +- `fill-clear`: a fill done as a `vkCmdClearColorImage` of a storage image, between two layout barriers (`StorageTexture::FillClear`). Filled bytes. +- `copy`: the buffer copy of `VulkanDevice::CopyBuffer`, a whole-surface image copy (`StorageTexture::CopyFrom`) and the depth plane copies to and from storage images through a staging buffer (`copyPlane` and the layered copy in `DepthSurface.cpp`). Copied bytes. +- `staging-in`: a build's copies of guest buffer regions into staging or host-copy buffers, between two barriers (`GuestBufferMemory::recordGpuCopies`). Copied bytes. +- `staging-out`: the copies of shader-written sub-ranges back into the import (`GuestBufferMemory::RecordCopyBacks`). Copied bytes. +- `draw`: for a lean draw, from its first barrier to the end of its render pass, so draws that continue the pass fall inside one range (`recordDraw`, ended by `Recorder::endOpenRenderPass`); for a recorded draw with layout transitions, from its first barrier to its download barrier (`Draw`). 0 MiB. +- `storage-upload`: detiles of guest bytes into a storage image, whole (`StorageTexture::upload`) or by changed runs (`StorageTexture::uploadWindows`), and pending units copied from an alias image (`StorageTexture::borrowUnits`). Uploaded bytes. +- `storage-writeback`: retiles of a storage image back to guest memory, by windows (`StorageTexture::writeBackWindows`) or by layers (`StorageTexture::writeBackLayers`). Stored bytes. +- `dcc-clear`: a `vkCmdClearColorImage` of a storage image whose DCC keys read as a clear, at upload (`StorageTexture::upload`) or for a fill of its key bytes (`StorageTexture::clearByKeysFill`, with `APS5_KEYS_FILL_CLEAR=1`). Surface bytes. +- `dcc-keys`: queued DCC key stores, as `0xff` fills and seed copies between two barriers (`Recorder::recordKeyStores`). Key bytes. +- `present-blit`: the presentation command buffer from its first to its last command, timed in its own two-query pool (`VulkanDevice::present`) and added when its slot retires (`Recorder::AddGpuTiming`). Width x height x 4 bytes of the swapchain extent. +- `shadow-publish`: copies of unit shadow slabs into the import, with their barriers (`recordCopies` in `UnitShadow.cpp`, reached from `PublishShadow`). Copied bytes. +- `template-refresh`: the `vkCmdUpdateBuffer` updates of a cached dispatch's changed data words, as one range from the first update to the last (`ShaderResources::refreshData`, called from `recordDispatch`). Refreshed bytes. + ## 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 fe31e07c2f..e66bf9fcad 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -14,7 +14,7 @@ A library with no scanned definitions and no shared sources, such as one whose ` ## History -[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` (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). The chart plots counts, not percentages: implemented and declared library functions and decoded shader instructions, so it shows how much is added over time; the percentages are on the progress map and the badges. 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. +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages: implemented and declared library functions and decoded shader instructions, so it shows how much is added over time; the percentages are on the progress map and the badges. 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 diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md new file mode 100644 index 0000000000..8297f4ab04 --- /dev/null +++ b/docs/dev/TRACING.md @@ -0,0 +1,74 @@ +# Tracing + +Set a switch before starting the emulator. Unless noted, any value enables it. Most output goes to the emulator log or standard error. Performance captures are written to files as described in [Build](BUILD.md#performance-trace). Traces can be large and can affect timing. + +## Kernel and libraries + +| Switch | Output | +| --- | --- | +| `APS5_TRACE_AFFINITY` | Thread role, host thread ID, requested CPU mask and whether pinning succeeded. | +| `APS5_TRACE_AJM` | Submitted audio jobs, decoded output and unimplemented AJM calls. | +| `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. | +| `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | +| `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | +| `APS5_TRACE_CALLS` | On Windows, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | +| `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | +| `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | +| `APS5_TRACE_PREPARATION_WAIT` | Shader preparation tasks moved to the front of the queue to satisfy a waiting caller. | +| `APS5_TRACE_SSE4A` | Each emulated SSE4A instruction once per guest instruction address (Windows). | +| `APS5_TRACE_SYNC` | Event-flag operations and their wait and wake results. | +| `APS5_TRACE_TIMERS` | Timing measurements for the host wait primitives used by the emulator. | +| `APS5_TRACE_USLEEP` | Every 2,000 guest `usleep` calls, with caller addresses and requested delays. | +| `APS5_TRACE_VIDEODEC` | Video decoder API calls and decode results. | +| `APS5_TRACE_WAITS` | Blocking waits aggregated by guest call site, including timeout counts and longest waits, every three seconds. | + +## GPU and memory + +| Switch | Output | +| --- | --- | +| `APS5_TRACE_BIGBUF` | Each copied or written-back guest-memory range of at least 1 MiB. | +| `APS5_TRACE_BDA` | Shader memory accesses that make a program use the buffer-device-address path. | +| `APS5_TRACE_BUFFER_WRITTEN` | Guest buffer elements classified as writable or read-only after descriptor population. | +| `APS5_TRACE_CAPSYNC` | Waits for GPU writes needed by CPU accesses, including submission and range details. | +| `APS5_TRACE_CONTEXT_STATE` | PM4 context-state operations and registers written while context state is pushed. | +| `APS5_TRACE_COPY_HLE` | Pending host-side copies, their ranges, GPU batches and known writers. | +| `APS5_TRACE_DCC_KEYS` | Display and storage-image DCC key reads, writes, fills and clears. | +| `APS5_TRACE_DISPATCH_CACHE` | Compute dispatch cache hits, misses and changed user-data words. | +| `APS5_TRACE_DISPATCH_IO` | Compute dispatch resources; value `2` also prints small user-data buffers. | +| `APS5_TRACE_DRAW_CACHE` | Draw-cache lookups, hits, misses, and key differences, reported every ten seconds. | +| `APS5_TRACE_DRAWS` | Draw inputs and per-draw timing phases. With `APS5_PROFILE_DRAW`, also prints the detailed draw lines. | +| `APS5_TRACE_DRAW_PROGRAMS` | One line per draw listing selected shader stages, roles, code addresses and entry addresses. | +| `APS5_TRACE_FILL_COVER` | Initial fill/image coverage mismatches and refused image-layer clears. | +| `APS5_TRACE_FLUSH` | Storage images flushed to guest memory and ranges left for the CPU. | +| `APS5_TRACE_GPU` | Queue submissions, packet execution, waits and display-buffer scanouts. | +| `APS5_TRACE_HOST_IMPORT` | Host-import mapping attempts, memory-map details, failures and retirement. | +| `APS5_TRACE_IMAGE_TABLE` | Image-table resource materialization and descriptor selection. | +| `APS5_TRACE_INDIRECT_DRAWS` | Indirect draw packets that pass the draw precheck. | +| `APS5_TRACE_LABEL` | `RELEASE_MEM` packets that target the address in this hexadecimal value. | +| `APS5_TRACE_LATE_LABELS` | GPU label writes observed after their waiting submission was handled. | +| `APS5_TRACE_QUERY` | Every sixteenth guest-memory page query, including cost and caller address. | +| `APS5_TRACE_RESOURCE_KEY_MISSES` | Resource-content key changes that cause draw or dispatch resource-cache misses. | +| `APS5_TRACE_SHADOW` | Unit-shadow publish, seed, eviction and retirement events; a number limits publish output. | +| `APS5_TRACE_SKIPS` | GPU operations skipped by diagnostics, with a bounded number of recompile-request dumps per reason. | +| `APS5_TRACE_STAGING` | Distinct guest-buffer ranges admitted to the write-staging window. | +| `APS5_TRACE_TEXTURES` | Sampled texture addresses, dimensions, format and nonzero-byte counts. | +| `APS5_TRACE_UNREADABLE` | Call sites that check inaccessible guest-memory ranges or fail to read them. | +| `APS5_TRACE_UPLOAD` | Reasons storage-image data is uploaded again. | +| `APS5_TRACE_BARRIERS` | Set to a positive integer to print that many skipped barrier decisions and their ranges. | + +## Performance captures + +See [Build](BUILD.md#performance-trace) for the trace format, summary fields and capture limits. + +| Switch | Output | +| --- | --- | +| `APS5_TRACE_FRAMES=:` | Records a window of presents by zero-based frame number. | +| `APS5_TRACE_SECONDS=:` | Records a window of presents by elapsed seconds after the first present. Cannot be set with `APS5_TRACE_FRAMES`. | +| `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | +| `APS5_TRACE_EVENTS=` | Sets the per-thread event-buffer capacity for a frame or seconds window (1–4,194,304; default 262,144). | +| `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | + +## Shader registers + +`APS5_TRACE_SHADER_REGS=all` prints register snapshots for every shader. Set it to a shader code address in hexadecimal to print one shader's registers. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2f721015f3..cfbf2e14f0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -20,7 +20,7 @@ Throughout the project, every function at every stage either **does exactly what - `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters; of the VoiceQoS ones only those still throwing remain so ### Functional diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 81b3f5e718..7d871117ed 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -20,16 +20,17 @@ - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords -- [dbOlWdppb4o](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping_0100`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware +- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown - [sceAgcDcbPrimeUtcl2](../../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceAgcDcbQueueEndOfShaderActionGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) -- [vieBRwlh1Lw](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only +- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`vieBRwlh1Lw`), not in the aerolib NID list; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) -- [vieBRwlh1Lw](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` +- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `sceAgcCreateInterpolantMapping_10000` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `sceAgcCreateInterpolantMapping_10000` here take it from `default_value` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known +- [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified ### Functional diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 574163fd4a..c89bf5c5de 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -1,7 +1,13 @@ # libSceAgcDriver technical debt +### Silent stubs + +- [sceAgcDriverRegisterDefaultOwner](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - takes one uint32 owner by value (OpenAGC `include/agcdriver.h`, SharpEmu `AgcExports.DriverResources.cs`) and returns 0 without recording it. PPSA21480 treats the code 0x8A6C9018 it returned before as fatal. Console behaviour is unverified: OpenAGC describes the function as a firmware 5.50 stub that returns 0x8A6C9018, SharpEmu returns 0 + ### Unknown function info +- [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured. + - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. - [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. @@ -11,31 +17,39 @@ - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. +- [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots, because Mesa's lowering exports one primitive, possibly null, per emitted vertex; whether the console's compiler does the same is not known, and triangle strips keep `- 2` per input. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` as for triangles. Line output counts as lines for the polygon offset decode, as on the vertex path. The lines are drawn 1 pixel wide with Vulkan's default line rasterization, like lines on the vertex path: `PA_SU_LINE_CNTL` is not read, and neither its width nor the console's line rules were measured. Points, rectangle lists, per-stream types and line strips from a vertex program on the mesh path still throw. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. Points, lines, a depth target and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen only on PPSA29413 and not verified on the console; other vertex-path values with `ES_EN` set still throw. - [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. +- [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. - [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 headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown +- [Registered shader types](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - types 0, 1, 2, 4, 6 and 7 are prepared and type 5 is skipped. Any other type, such as type 8, is logged (`unsupported registered shader type 8; it is not prepared at registration`) and left unprepared at registration, and throws when it is used later. Hello Neighbor 2 (PPSA07426), Ad Infinitum (PPSA10600) and Ghostrunner (PPSA03685) register type 8 headers and run on past them; what a type 8 header describes is unknown, #1892 found no context registers in it. - [NGG vertex workgroups](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. - [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown - [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. +- [Depth and stencil plane fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill that covers a known depth surface's whole stencil plane (`DepthSliceBytes` of its extent at one byte per texel) with four equal bytes clears the host stencil to that byte the next time the surface is bound, sampled or stored to, and a fill that covers the whole depth plane (two or four bytes per texel) clears the depth when the dword is a float in 0.0-1.0, or two equal 16-bit halves read as UNORM for D16 (`NoteDepthMetadataFill`). A partial fill, a dword whose bytes differ, a depth value outside 0.0-1.0 (0xff000000 is seen), a multisampled surface, or a fill made before the surface exists leave the host image as it was. An HTILE fill or a recognized HTILE clear kernel noted after a plane fill replaces its value with DB_DEPTH_CLEAR/DB_STENCIL_CLEAR. On the console the fill writes the plane the depth block reads; the surfaces this was seen on keep the stencil out of HTILE (PPSA26525 Heretic + Hexen and PPSA21444 DOOM + DOOM II reset the Cohtml UI stencil plane to 1 with a 0x01010101 fill every frame, then draw with stencil Equal 1 and 2), and what the console returns for a plane fill under a compressed HTILE was not measured. `agc_driver_depth_plane_fill` pins the rules. - [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 +- [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. + - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. -- [Type-0 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 in dword 3) with a nonzero base are bound as null images, like a zero base. The behaviour comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault); it was not measured on the console's gfx1013. +- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. 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. - [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. @@ -56,21 +70,24 @@ - [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. +- [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only and never compacted, so a module a later run no longer builds pipelines from stays in the file until it is deleted, and the whole file is read into memory while the replay runs (its modules reference that buffer until the last replayed pipeline was built). Only the first `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines recorded are replayed (a record is never rewritten, so a pipeline past the bound stays past it), and the pipeline store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES` (1024 each): a title with more configurations rebuilds the rest at the draw as before. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, which a dispatch that finds it resident checks against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost, an entry whose build fails is skipped, and both are counted in the `[pipeline-journal]` line rather than stopping the run. A replayed pipeline that the title never asks for again costs its build time and its driver memory (its optimized relink is deferred to its first draw, so a run whose first frames bind many replayed pipelines queues their relinks then); nothing ages recipes out of the file. Two runs sharing the file take turns under its lock file, but each only knows the modules it loaded or wrote itself, so a module the other run appended meanwhile may be appended again (the loader keeps the first copy). Records a run appends after another run's load are replayed by that run only after its next start. The file header's format number is the only guard against a change of the recipe codec, `CanonicalPipelineState` or the store key that keeps the structures' sizes (the recompiler's source version does not cover the driver's sources). - [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 - [Prebuilt pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [`APS5_THREAD_PRIORITY=rt`](../../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy -- AMD image descriptors with `TYPE=0` are treated as null for read-only image slots even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and reserved `TYPE=1` also faults (gfx1035 reads it as zeros too); gfx1013 behavior is unverified +- AMD image descriptors with `TYPE=0` are treated as null for read-only image slots even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and the reserved types 2-7 also fault. `TYPE=1` is treated as null in the same read-only slots: gfx1035 reads it as zeros too, and gfx1010 returns zeros for `image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` with format 22 (#5828); it is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). gfx1013 behavior is unverified - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory of 1 MiB or more when the import probe keeps the write watch and a settle queue exists (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists, and an import of under 1 MiB, is compared instead: the settle blocks the importing thread, and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). Settling without blocking the importing thread (submit the read, keep the import compared until its fence signals, then switch it to the write watch) is not implemented. +- [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. - [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. @@ -85,7 +102,7 @@ - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. Last mode throws without `VK_EXT_provoking_vertex`, for generated mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. @@ -104,9 +121,10 @@ - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. -- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, 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. +- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. 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 use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. 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. +- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [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. @@ -116,6 +134,7 @@ - [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. +- [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, 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), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [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. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. @@ -127,12 +146,13 @@ - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. -- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). +- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. - [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - an S# that forces sRGB decoding adds nothing to an sRGB texture, as in shadPS4 (PS4). Measured on RDNA2 (gfx1035), not on the console: `8`, `8_8` and `8_8_8_8` sRGB textures sample bit for bit the same with and without the bit (point and bilinear; BC formats not measured). Other textures throw; on the same hardware their UNORM `8`, `8_8` and `8_8_8_8` forms sample bit for bit as the sRGB format (decoded before filtering). - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - The [driver](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) binds at most 262144 table images per pipeline, fewer under lower sampled-image limits; the rest fault when selected. A device without runtime descriptor arrays or variable-count bindings throws. +- [Image storage tables](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - every bound element of a storage table is marked written and stored back after each dispatch, whether or not a lane wrote it. Two elements over the same bytes are therefore both stored back, and the order of their write-backs decides the result: PPSA26344's compute 0x80001e5f00 binds the stencil plane at 0x505f350000 twice, as R8_UINT in its own tile mode and as R8_UNORM in another. Rejecting the second writer of the same bytes per element, and per-element written bits from the GPU, are not implemented. The storage table binding is declared with its full capacity, so a set that holds one does not fit a chain descriptor pool and gets a dedicated pool. That capacity is a choice, not a device limit: the smallest of the sampled table capacity, 4096 and a quarter of `maxPerStageDescriptorStorageImages`. The sampled table capacity is not reduced by it, so a device with tight storage-image limits can fail layout validation. A storage element is rejected when its absolute index in the set reaches that capacity, even when few of the set's table elements are storage: the index counts every table element before it in the set, sampled ones included, and the element faults when a lane selects it. Atomic, packed, BY2/BY4 and dynamic-mip stores through a table still fault on use. `PresyncSurfaces` walks only the sampled table, so a store-only table's images are not presynced. Tables that mix sampled and storage elements, a T# that is both read and written through a table, non-uniform storage indexing and non-2D storage modes are not covered by tests. - [Image-table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) reading opaque black borders fault on selection: no runtime BC-swizzle pair check, even for valid swizzles 0/4 with direct or table T#s. Unselected entries do not fault. Table T#/direct S# keeps the direct check: only opaque-black swizzles moving alpha fault. - [Table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) that clamp to border fault only when selected: four variants cannot encode dynamic integer/float pairing. This also rejects otherwise-valid float pairs. - [Table T#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) paired with direct S#s fault when a paired border class differs from the decoded image class. Depth-bit views use float borders. Matching float pairs and inactive mismatches do not fault. @@ -150,3 +170,5 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. + diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 0546015bae..bcdfde9b1e 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -2,11 +2,13 @@ ### Unknown function info +- [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw +- [AJM decoders other than MP3, ATRAC9, M4AAC and Opus](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - decoding throws for every other codec id. Broken Sword - Shadow of the Templars: Reforged (PPSA21480) stops at codec 23 (`AJM: decoding codec 23 is not implemented`). The ids known here are MP3 0, ATRAC9 1, M4AAC 2 and Opus 24; id 23 is not among them and which codec it is, is unknown. - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index db01df9a8b..68774cc30e 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -7,6 +7,7 @@ - [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 +- [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled ### Functional diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 957dc51912..5639476d66 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,10 +3,9 @@ ### Unknown function info - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB -- [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; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified +- [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified - [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/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 90d7a053d8..09f5bce9c1 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -7,4 +7,5 @@ ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too +- [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 75a1c10469..e1469b061a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. More than 16 completed grains awaiting Push and a nonblocking Push without a completed grain throw; the limit and errors are runtime restrictions, not console measurements. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. +- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. - [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), [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 - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown @@ -25,4 +25,5 @@ - [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. - [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/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 6e1bcb974e..0e37f5e79f 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -3,7 +3,9 @@ ### Unknown function info - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the HE-AAC handling follows one console measurement of an HE-AAC v2 ADTS stream: SBR output at 48 kHz in one channel with `ui_enable_heaac` 1, the 24 kHz mono core with 0. Not measured: whether the console, like FFmpeg, decodes a stream without SBR data in its first access unit as AAC-LC with `ui_enable_heaac` 1, and what it reports in `ui_sampling_freq` and `ui_heaac` with `ui_enable_heaac` 0 (here the rate of the returned PCM and 0) ### Functional +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - an ADTS stream with channel configuration 0 (layout in a program config element) is still parsed by FFmpeg, so `ui_enable_heaac` 0 does not remove its SBR and a mono one with Parametric Stereo comes out as stereo. - [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 5b8396edbf..148f5db253 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,6 +7,8 @@ ### Unknown function info +- [AvPlayer loop warnings](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) are queued when the first audio or video frame of a loop is delivered, following [KytyPS5's playback boundary tracking](https://github.com/KytyPS5/KytyPS5/blob/a6f4e8e7f456b8697d3102823d3f16400c3e121e/src/libs/avPlayer.cpp#L1038-L1043). The console's exact callback timing is unverified. Decode-ahead does not announce a loop while the caller is consuming the previous pass. + - [sceAvPlayerAddSource](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceAvPlayerStartEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index e771fdeec7..8c1cabe7b5 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -1,5 +1,5 @@ # libSceContentDelete technical debt -### Unknown function info +### Functional -- [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 +- [sceContentDeleteById and sceContentDeleteByPath](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) delete from a content library that is empty on the host, as nothing is exported to it, so both throw for every content id and path once the arguments and the initialization are checked: no reference gives the console's error for content that is not in the library ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentDelete.cs#L25-L42) names only `0x809D5006`, no permission). The signatures, a 64-bit content id and a UTF-8 path, follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L541-L542) and SharpProspero, which agree. A null path and a call outside `sceContentDeleteInitialize` and `sceContentDeleteTerminate` throw, as the console's errors for them are unknown, and the order of the two checks is unverified. diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 1f1b491f21..458e175688 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown; a value other than 0 throws, and so does a keyboard type other than 0. +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PRAGMATA (PPSA02530, #5726) and Onimusha: Way of the Sword (PPSA27836) pass `0x4000000` as a constant: Onimusha calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. The returned character for HID usage `0x64` has not been measured on a console; `\` is an unverified US-layout guess based on the Windows VK mapping, which does not establish the console result. ### Functional diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index 0a64c52ca5..2e52af3853 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -7,3 +7,4 @@ ### Unknown function info - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured +- [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index c2b60afaf4..15c10ccaf8 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -16,6 +16,7 @@ - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. +- [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown ### Functional diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index 7d7e234519..d82804dae6 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched +- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` returns `0x80412803` as in prosper; KytyPS5 accepts it instead, and neither is confirmed on a console. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index aab10875a0..9b27109ad4 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -7,6 +7,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) +- PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVrTracker2GetCoordinateSystem and sceVrTracker2GetPlayAreaBoundaryGeometry](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [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/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 25cae419d8..c260aa6a53 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -21,4 +21,6 @@ ### Functional +- [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. + - [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 8511d88e3d..76178899b3 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -11,5 +11,6 @@ ### Functional +- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 8f71262efb..f723880a61 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -10,5 +10,6 @@ ### Functional -- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), whose return type, and so whether a result pointer precedes `this`, is unknown, and [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [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) +- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), whose return type, and so whether a result pointer precedes `this`, is unknown - [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console diff --git a/docs/dev/debt/libSceLoginService.md b/docs/dev/debt/libSceLoginService.md new file mode 100644 index 0000000000..f33713e90d --- /dev/null +++ b/docs/dev/debt/libSceLoginService.md @@ -0,0 +1,9 @@ +# libSceLoginService technical debt + +### Silent stubs + +- [sceLoginServiceInitialize, sceLoginServiceTerminate](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - return success and keep no login-service state: AnyPS5 has no PSN login service. EA SPORTS FC 27 (PPSA34015) loads the module with `sceSysmoduleLoadModule(0xe3)` and calls `sceLoginServiceInitialize` with no arguments; a missing `libSceLoginService.prx` stops Windows start-up with `Failed to load module` / `GetLastError: 126` + +### Unknown function info + +- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - PPSA34015 passes one pointer to a stack buffer (`lea rdi, [rbp-0x80]`) then calls `sceLoginServiceTerminate`. The buffer layout, other arguments and console errors are unknown, so every call throws without reading or writing caller memory diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md new file mode 100644 index 0000000000..a26decfa8d --- /dev/null +++ b/docs/dev/debt/libSceMouse.md @@ -0,0 +1,5 @@ +# libSceMouse technical debt + +### Functional + +- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - signature taken from a call site (a handle, then x and y as 16-bit values: PPSA30601 passes the centre of its display size); behaviour not measured on hardware. Validates initialization and the handle like `sceMouseRead` and returns `MOUSE_OK`; the position is not stored because the library reports relative motion only diff --git a/docs/dev/debt/libSceMsgDialog.native.md b/docs/dev/debt/libSceMsgDialog.native.md index 8f8116bec7..bcd6bc6be9 100644 --- a/docs/dev/debt/libSceMsgDialog.native.md +++ b/docs/dev/debt/libSceMsgDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog +- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4; null pointer arguments return `COMMON_DIALOG_ERROR_ARG_NULL`. diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fae79da01e..e101cbca34 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -1,17 +1,22 @@ # libSceNet technical debt +### Silent stubs + +- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs both the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified + ### Unknown function info -- PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) 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. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator -- [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters ### Functional - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it +- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 4da9e097aa..f7effa839a 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -1,5 +1,9 @@ # libSceNetCtl technical debt +### Silent stubs + +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `sceNetCtlGetNatInfo` validates null pointers and returns `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) without performing NAT discovery. + ### Unknown function info -- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The Wi-Fi codes (5-10), the DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server and port (20, 21) throw +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the HTTP proxy port (21) is 0; the Wi-Fi codes (5-10) throw diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 812d7348b3..9513088078 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -2,6 +2,8 @@ ### Unknown function info +- [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. + - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. - [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. @@ -15,16 +17,21 @@ - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) -- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` +- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason -- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented -- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented -- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. +- [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` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits +- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted; unknown flags, zero or parallel orientation vectors, nonfinite input and an unrepresentable translation throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM alone. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. +- [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. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified + +### Functional + +- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value is read from the PS5 `libSceNgs2.native.sprx` export `MwmHz8pAdAo` (firmware 10.01 at `0x11f10`, 12.70 at `0x12130`, and every module from 1.00 to 12.70 checked in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590)): an index at or past the rack's voice count returns it through the same error helper that answers `0x804A8010` for a null output, and the output handle is written only on success. The rack is checked before the output pointer, and an invalid rack returns `0x804A8202`. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index cb0212dd00..8076229ee3 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -3,7 +3,10 @@ ### Unknown function info - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. +- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory. +- [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `WxLcN76q19o`) - the name is not in aerolib. PPSA34015 imports it from `libSceNpManager.prx` as a strong `JUMP_SLOT` with no call found in the eboot; the loader still requires the export. Arguments, result and console errors are unknown, so every call throws. - [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored +- [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user ### Functional diff --git a/docs/dev/debt/libSceNpPartner001.md b/docs/dev/debt/libSceNpPartner001.md index a8a1d8b242..27edb961fb 100644 --- a/docs/dev/debt/libSceNpPartner001.md +++ b/docs/dev/debt/libSceNpPartner001.md @@ -3,4 +3,4 @@ ### Functional - [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 +- [sceNpHasEAAccessSubscription](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - `(int userId, bool* result)` from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp) and PPSA34015's only call (`mov edi, r14d`; `lea rsi, [rbp-0x67]`). Writes `false` and returns 0: AnyPS5 has no EA Access server. Not initialized or a null result throw where shadPS4 returns the PS4 errors `0x819D0001` / invalid argument, whose PS5 values are unverified. Whether the console errors while signed out, and what it writes when the user has a subscription, are unknown diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 9f9b7ad514..c23308b9fe 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -6,3 +6,5 @@ - [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 +- PPSA31327 imports declared without parameters, signatures unknown: [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling), imported by Yakuza 0 Director's Cut and not called before the main menu. + diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 8e1fa7ef7d..142e30364e 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). + +### Unknown function info + +- [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) takes `(int context, int handle, int reward_id, void* buffer, size_t* size)`, from the call sites in the bundled `Media/Plugins/PSNCore.prx` of PPSA06888, the same shape as `sceNpTrophy2GetTrophyIcon`. The title calls it twice: first with a null buffer to read the size, then with a buffer of that size allocated with `malloc`. The meaning of the third argument is unverified, and the reward icon data is unknown, so every call throws diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 9e8e3461c6..c805a3decc 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -6,6 +6,6 @@ ### Unknown function info -- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. +- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) - the two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. An unknown library context throws, as in `sceNpWebApi2PushEventSetHandleTimeout`. The handle is not checked and nothing is aborted, as push event handles are not tracked and nothing waits on a handle offline - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws - [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 74c417e67e..9273851ee1 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -2,5 +2,6 @@ ### Functional -- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. +- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap +- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index b828591f2b..573dc9d373 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,6 +2,7 @@ ### Unknown function info +- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - firmware 13.60 returned `0x80920005` before `scePadInit` and `0x809200ff` after it in the #3177 probe; the tested call read or wrote no argument pointers. PPSA15645 calls the import after `scePadOpen`, but its return and side effects in that state are unknown. The NID name and signature are unknown, so the statuses are not named by presumed error meanings - [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 @@ -9,3 +10,4 @@ - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown +- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - PPSA01530 and PPSA13356 import it and fail to load without it. The real name is unknown; it is declared without parameters and throws diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index b7dcfacd48..ef94e4f939 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,5 +2,5 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 3042611621..d30a44b812 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,8 +2,13 @@ ### Unknown function info +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The other context parameter fields (a 16-bit size pair at 0x18, two descriptor pointers at 0x20 and 0x28) are not read. The requirement of four 2 MiB blocks, 2 MiB aligned, is what PPSA08330 v06.006 allocates regardless of the returned count; Sony's values are unknown ([KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libPsml.cpp) derives 52 blocks for type 0; its layout differs from these calls: it writes a 0x30-byte object through the first argument, where PPSA08330 passes one of two adjacent 8-byte handle globals, and reads the shared resources at context parameter offset 8, where both titles pass the block list). Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters -- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned +- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified +- [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source +- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) - [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K) - imported by PPSA01317, which fails to load without them; signatures unknown, so they throw. The names are not in the aerolib NID list; they hash to the imported NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw` (`tools/nid_names.py`) -- [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`) + +### Functional + +- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. Whether Sony's library fails on hardware without PSSR is unknown. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0, so the dispatch, mip bias and capture calls still throw diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 0c42c424a8..30115eb282 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -10,3 +10,4 @@ - [sceRazorCpuFlushOccurred](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 +- [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index f2e4d7383b..38a4add132 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -10,3 +10,5 @@ - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too +- PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) + diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index ca4be61ba7..042e5e2109 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown - [sceSaveDataPrepareForTransferring](../../../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. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [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 @@ -15,4 +16,6 @@ - [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` +- [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware +- [sceSaveDataGetMountInfo, sceSaveDataGetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup diff --git a/docs/dev/debt/libSceSaveDataDialog.native.md b/docs/dev/debt/libSceSaveDataDialog.native.md index a06b238913..92ab4e7f4c 100644 --- a/docs/dev/debt/libSceSaveDataDialog.native.md +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. +- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`, and progress updates (`sceSaveDataDialogProgressBarInc`, `sceSaveDataDialogProgressBarSetValue`) return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running. diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index f5b9ed5354..abc8332e1b 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -4,10 +4,6 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate readers remain unimplemented. Loading credentials neither starts a TLS connection nor authenticates a peer -- PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). Connection storage exists, but their signatures and option behavior remain unknown, so they still throw - -### Functional - -- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslEnableVerifyOption](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350). Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials neither starts a TLS connection nor authenticates a peer +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md new file mode 100644 index 0000000000..12ced1520f --- /dev/null +++ b/docs/dev/debt/libSceSysmodule.md @@ -0,0 +1,9 @@ +# libSceSysmodule technical debt + +### Silent stubs + +- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept a module id missing from the module table: they log it and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports an unknown module as loaded and unloading it succeeds, because titles that load or unload such an id aborted otherwise. The internal variants (`sceSysmoduleLoadModuleInternal`, `sceSysmoduleUnloadModuleInternal`, `sceSysmoduleLoadModuleInternalWithArg`, `sceSysmoduleGetModuleHandleInternal`) throw for an unknown id. What the console returns for an id it does not know is not verified. + +### Functional + +- [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - on Windows the region and file name of an address inside a loaded image are cached after the first lookup, because `VirtualQuery` on a large image section is slow. An entry is used only while the loader still reports the same image for the address (same base, time stamp and size). A page protection change inside the image after the first lookup, which would split or merge the region `VirtualQuery` reports, is not noticed. Addresses outside loaded images are queried every time diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index 98903ec7d4..dadc3f4a07 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,4 +2,4 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists, so a title never receives a gesture event. Handle and pointer arguments are still checked. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known +- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists, so a title never receives a gesture event. Handle and pointer arguments are still checked across all query entry points including `sceSystemGestureGetTouchEventByIndex`. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3e5bcecac2..8d8ea62347 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -11,10 +11,12 @@ - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown -- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed +- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC ### Functional -- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, unconverted executables and macOS hosts throw +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors; as during `exit`, a guest thread that calls a library the shutdown has stopped waits for the end instead of unwinding. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, 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. +- [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 3ac4a39278..95e0067125 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -5,6 +5,7 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified +- [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured ### Functional diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index 09e1b11228..bffe3f23ab 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -2,4 +2,8 @@ ### Unknown function info -- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown +- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. `sceVdecswGetHevcPictureInfo` still throws: the 0xb8-byte HEVC picture layout is in shadPS4's `vdecsw.h`, but this wrapper only keeps AVC picture info from Videodec2 + +### Functional + +- [sceVdecswGetHevcPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - throws until HEVC picture info is filled from the decoder diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 6eb7fa25dc..8a5caa007a 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,17 +9,19 @@ ### Unknown function info +- [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [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 +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 - [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 +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [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 - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data +- [sceVideoOutRegisterBuffers, sceVideoOutGetFlipStatus, sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - PS4 callers. A handle is treated as PS4 once buffers are registered through the PS4 `sceVideoOutRegisterBuffers`; from then on its flip and vblank status are written in the PS4 64- and 32-byte layouts, with `tsc` and `submitTsc` 0. A PS4 title that reads a status before it registers buffers gets the larger PS5 layout. The PS4 function accepts only linear A8R8G8B8_SRGB (0x80000000) and A8B8G8R8_SRGB (0x80002200) buffers without options; tiled PS4 buffers throw, as their layout differs from the PS5 tiling. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample registers its second 1920x1080 buffer 8,294,400 bytes after the first. The PS4 alignment rule is not known ### Functional diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 5d7b802774..3adaaa4524 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -6,7 +6,10 @@ ### Functional +- [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are + - [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` is not exported; `sceVideodec2GetVp9PictureInfo` has its own VP9 layout - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Existing NV12 output rejects pictures deeper than 8 bits. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. +- [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index e86f72ff0f..8d57d43d4a 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -1,5 +1,10 @@ # libSceVoice technical debt +### Silent stubs + +- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - attribute 1001 writes one zero byte and returns 0 for an existing port; the backend processes no voice audio. PPSA34501 requests size 1 and reads one byte on success, and this fallback avoids its startup exception. The name follows attribute 1001 (`CELLVOICE_ATTR_VAD`) (Voice Activity Detection) in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellVoice.h), not confirmed for the PS5. The correct result, eligible port types and behaviour are unknown. + ### Unknown function info - [sceVoiceResetPort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(uint32_t portId)` signature is the PS Vita one in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L503-L514) and the PS3 `cellVoiceResetPort` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L493-L510); that the PS5 library takes the same argument is assumed. A reset clears a port's buffered data, and ports here buffer nothing (`sceVoiceReadFromOPort` returns 0 bytes and `sceVoiceWriteToIPort` throws), so it only checks that the library is initialized and the port exists, as RPCS3 does, and keeps the port's connections, volume and mute flag. An uninitialized library or an unknown port throws like the other port calls; the console's error codes are not measured. PPSA23566 imports it +- [sceVoiceGetResourceInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(VoiceResourceInfo* info)` signature and port limits (7 voice inputs, 7 voice outputs, 1 device input, 1 device output, 64 total ports) are the PS Vita ones in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L150-L157); that the PS5 library uses the same layout and limits is assumed. An uninitialized library or a null output pointer throws like the other Voice calls; console limits and error codes are not measured. PPSA23566 imports it diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 7b3937b6a9..a62a38f2d7 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,8 +3,8 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws ### Functional -- [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented +- [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; here it drops every endpoint and connection diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md new file mode 100644 index 0000000000..bc07026490 --- /dev/null +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -0,0 +1,5 @@ +# libSceWebBrowserDialog technical debt + +### Silent stubs + +- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The real export takes a second argument, the allow-list of up to twenty addresses (SharpProspero `docs/dialogs.md`); it is not read here, so no address filtering is modelled, and the exact validation order and codes with a null second argument are unverified. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..c75e2fbd90 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,7 +2,10 @@ ### Unknown function info +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. +- [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use the registered application allocator. No console measurement; null allocation returns null and other allocator errors throw. + - [_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. @@ -24,26 +27,36 @@ - [memcpy_s and memmove_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 K.3.7.1.1 and K.3.7.1.2 size constraints, with RSIZE_MAX taken as SIZE_MAX >> 1 as in memset_s. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. +- [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. - [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks - [vsnprintf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title +- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified - [__udivti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc, re-exported by libSceAmpr) - the hardware traps on a zero divisor; this throws instead - [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead +- [__modti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead ### Functional +- [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. + +- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. + - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. -- [getc_unlocked, putc_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [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 +- [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [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 +- [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured - 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; @@ -58,7 +71,7 @@ - [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` 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. +- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [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.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` @@ -69,9 +82,10 @@ - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. Nothing passes imports through `TraceImport_nid_no_patch` yet, so no title is traced. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. +- [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` +- [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b93b7ccea5..02c8d01b37 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -4,7 +4,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws -- [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in FreeBSD'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 +- [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) @@ -13,7 +13,6 @@ ### Unknown function info - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. -- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. Names other than `_SC_CLK_TCK`, `_SC_PAGESIZE`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` @@ -35,6 +34,7 @@ - [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) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signature +- [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - imported by PPSA16833, whose executable does not load without it; added without an implementation; unknown signature - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call @@ -47,7 +47,7 @@ - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected -- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one +- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` and Unity's `PS5Util.prx` (PPSA18194) pass `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and wait for a running call_once with operation 1. Whether the console reads a microsecond timeout there in all cases, and what operation 1 does on it, is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded @@ -70,9 +70,10 @@ - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [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 +- [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported +- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) takes `(void* threadContextInfo, size_t threadContextInfoSize)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 144-byte block of 18 64-bit registers follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs#L120-L141). It answers as `sceCoredumpGetStopInfoCpu`: the handler is never invoked here, so an aligned 144-byte request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block, and null, unaligned or wrapping buffers and other sizes throw, as the console validation order is unverified. Planet Coaster (PPSA01736) imports it - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants - [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: 007 First Light (PPSA11386) imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) - [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 @@ -97,7 +98,6 @@ - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [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 and priority of explicit scheduling attributes, or its creator's when it inherits, as it does with a null attribute - [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` @@ -105,10 +105,12 @@ ### Functional +- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [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`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond @@ -118,17 +120,20 @@ - [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 +- [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, 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, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws +- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. +- [pthread_condattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) stores a private or shared attribute, following FreeBSD 11 libthr [thr_condattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_condattr.c): a null or uninitialized attribute and values other than `PTHREAD_PROCESS_PRIVATE` (0) and `PTHREAD_PROCESS_SHARED` (1) return `EINVAL` without changing it. Guest threads execute in one host process; sharing condition variables across processes is not modeled. PSFlyCast (PPSA99247) imports it and fails to load without it. Console behavior is unverified. - [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 +- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [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 +- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [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 +- [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store +- [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only @@ -141,23 +146,26 @@ - [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. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [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 +- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. -- [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 +- [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured +- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. The other operations, and every operation on other hosts (Windows keeps its TEB in GS), throw. What a new thread's GS base starts as is not modelled: Linux threads inherit their creator's - [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.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) -- [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked, because Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE without reporting them through CPUID +- [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors ([`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio)), which guest files, pipes and sockets all take, and on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load. The console's limit is not known; XPSemu (PPSA97358) reads it in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK` and `RLIMIT_NPROC` throw because no console values are established. -- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. +- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds when its normalized values match `getrlimit`, or for `RLIMIT_NOFILE` when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. +- [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it @@ -166,20 +174,22 @@ - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, 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`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it -- [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. +- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` 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. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [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, sigsuspend, 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)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. Like `_sigprocmask`, only the first 32 signals are blocked: words 1 to 3 of the argument are not applied, so signals 33 to 128 stay deliverable during the wait; widening the shared mask is a follow-up. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point +- [pause, sigsuspend, 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)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five 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`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, 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 +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`, and Windows maps no files. @@ -197,3 +207,5 @@ - [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. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up +- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. +- [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8fb1dba028..4caf7940b7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -1,10 +1,13 @@ # libkernel_sys technical debt +### Silent stubs +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `AT_SYMLINK_NOFOLLOW` on a symbolic link (on Windows and Linux) + ### Functional - [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 +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [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 diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 9a7d01a870..1b585e36fa 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -9,6 +9,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. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. +- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) decodes a near CALL, JMP or Jcc with an operand-size prefix (`66`) as Intel does, with a 32-bit or 8-bit displacement and a 64-bit target. AMD processors, the console included, read a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` and truncate the target of every near branch with that prefix, rel8 forms included, to 16 bits. Compilers do not emit these forms; the relinker's length and target for them do not match the console. - `--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. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. @@ -16,6 +17,8 @@ - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE but reports none of them through CPUID, so code that checks CPUID takes its baseline path. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests; that the converted code runs under Rosetta was checked on an M5 Max only, outside this repository's CI. +- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, 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. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time + +- [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index cc4acdf6c2..dc6fd4ee94 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -70,7 +70,7 @@ - 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; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. +- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values @@ -83,23 +83,29 @@ - [NGG mesh entry](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. - [Mesh shader grid size](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). -- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. +- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders throws when that stage is built. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. -- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. +- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 throws with any f16 rounding but nearest even. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. -- [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. -- [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` are not measured and keep their previous lowering. Shaders without a known float mode keep the previous lowering. -- [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. -- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16` and `v_fmaak_f16` follow the same field at nearest-even: inputs are flushed when bit 0 is clear, and the result is flushed when bit 1 is clear if the exact product-sum rounded to 11 significant bits with an unbounded exponent is below 2^-14, so `0x0400 * 0x0400 + 0x8400` rounds to `0x8400` and is kept. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 5075 operand triples of `v_fma_f16` and `v_fmac_f16` (4000 from 20 halves around the denormal range, 75 whose product-sum lies just below 2^-14 and 1000 random), and over the first 4075 of them for `v_fmamk_f16` and `v_fmaak_f16`, FP16_OVFL=1, the clamp, neg and abs modifiers and op_sel. Directed rounding and `v_fma_mixlo/mixhi_f16` keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. +- [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. +- [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. +- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler +- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked +- [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. +- [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. @@ -116,7 +122,8 @@ - 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. +- [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. +- [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 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 (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. @@ -124,7 +131,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment and atomics throw. +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. @@ -140,9 +147,10 @@ - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) hold the pixel-centre barycentrics, like the centroid pairs. With one sample this is the sample position ([Vulkan standard sample locations](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-multisampling): (0.5, 0.5)); with more samples the console's per-sample weights are not computed and were not measured. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it (a VALU mask counts as one SGPR unless the request says wave64), and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [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. +- [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in zero-initialised function-scope storage per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. +- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 3032533866..882ce0a000 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -18,6 +18,31 @@ Game-controller bindings are enabled automatically and are not affected by keybo 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`. +### Default bindings + +Without an `anyps5-input.ini`, or for actions the file does not mention, these bindings apply: + +| Action | Keyboard and mouse | +|--------|--------------------| +| `Cross` | Return, Space | +| `Circle` | C | +| `Triangle` | I | +| `Square` | Left mouse button | +| `L1` | Q | +| `R1` | E, Left Alt, Right Alt | +| `R2` | Right mouse button | +| `L3` | Left Shift, Right Shift | +| `R3` | Left Ctrl, Right Ctrl | +| `Options` | Escape | +| `Up`, `Right`, `Down`, `Left` | Arrow keys; the mouse wheel also presses `Up` (wheel up) and `Down` (wheel down) | +| `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown` | A, D, W, S | +| `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown` | F, H, T, G | +| `TouchLeft`, `TouchRight` | Backspace, Tab | +| `ToggleMouse` | Middle mouse button | +| `ToggleFullscreen` | F11 | + +`L2` has no built-in keyboard or mouse binding. + For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: ```ini diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 957cb08714..9d1882c0bb 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -60,7 +60,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | | `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | +| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, 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. | | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | @@ -107,6 +107,8 @@ Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input direc Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. +Resources can also be packed into `app0.zar` beside `app0/`, a [ZArchive](https://github.com/Exzap/ZArchive) whose root is the `app0/` directory. A file present in `app0/` is read from there and any other file from the archive; the executable, `sce_module/` and `sce_sys/` stay in `app0/`. The archive is read only through `sceKernelOpen`/`open`, the read, seek and stat calls on its descriptors and `sceKernelStat`; other calls on an archived path, listing a directory that the archive also contains and opening a file in it for writing fail as described in [technical debt](../dev/debt/libkernel.md). An archive that cannot be opened stops the title at its first file access. + Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. @@ -142,9 +144,10 @@ python3 tools/pad_input.py /tmp/anyps5-pad down --hold-ms 500 python3 tools/pad_input.py /tmp/anyps5-pad release python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 +python3 tools/pad_input.py /tmp/anyps5-pad touch-right ``` -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### System fonts @@ -154,6 +157,17 @@ Games that open the console's system font sets (`sceFontOpenFontSet`) need font Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. +### PlayGo dumps without metadata + +When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario.json`, AnyPS5 cannot tell which nonzero PlayGo chunk IDs exist. Requests for those IDs stop with an error by default. Set `APS5_PLAYGO_UNLISTED_CHUNKS` to choose an answer for that dump: + +| Value | Answer for an unlisted chunk ID | Observed title that needs it | +|-------|---------------------------------|------------------------------| +| `installed` | Report it as installed (`LOCAL_FAST`). | High on Life (PPSA15388) needs this to load its saves. | +| `absent` | Return `SCE_PLAYGO_ERROR_BAD_CHUNK_ID`. | Sifu (PPSA03001) and Demon Slayer: Kimetsu no Yaiba - The Hinokami Chronicles (PPSA03490) use this result to end their chunk-ID search. With `installed`, PPSA03490 keeps asking about higher chunk IDs forever, so its chunk list grows until startup fails with a `Ran out of memory allocating 5565054976 bytes` error. | + +Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console. + ### GPU selection The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. @@ -177,7 +191,17 @@ HDR output is available on Windows only. Titles see it when the monitor showing ### Shader cache -Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Deleting the directory is safe: it is rebuilt on the next run. +Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. + +### Pipeline journal + +Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. + +### Texture cache budget + +Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves, the part of the sampled textures' 2 GiB floor not yet in use, and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. While the sampled textures alive stay within their floor the sampled budget falls first, to that floor, then the storage budget; above it both fall together. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. + +A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. ### Storing GPU results at each flip @@ -200,8 +224,16 @@ Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/ ### Windows crash diagnostics +When a register points at readable memory that starts with at least four printable ASCII characters, either as UTF-16LE or as single bytes, the crash report prints that text after the register memory dump, as `[r13] u"..."` for UTF-16LE or `[rdi] "..."` for single bytes. The text ends at a NUL, at the first character that is not printable ASCII, a tab, a line feed or a carriage return, after 512 characters or at the end of readable memory. Tabs, line breaks, quotes and backslashes are escaped, and `...` after the closing quote means that no NUL ended the text within those limits. + Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. +### GPU submission rings and stall dumps + +Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`. + +With `APS5_STALL_WATCHDOG_SECS=`, a queue that has waited `n` seconds without progress in a `WAIT_REG_MEM`, a `REWIND` or behind queue 0 prints the rings once for that wait, and the title keeps running. A `WAIT_REG_MEM` progresses while any other packet runs or completes, as for `APS5_GPU_WAIT_TIMEOUT_MS`. `n` is from 0 to 86400 and 0, the default, disables the watchdog; any other value stops the title at its first submission, as for `APS5_PM4_RING`. While the rings are printed for a queue held behind queue 0, new submissions and the other queues wait. + ## Exit codes `0`: conversion succeeded or help was displayed. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. @@ -230,6 +262,8 @@ Each registry has a companion with the same prefix and the suffix `.needed.json` The report also lists needed libraries that have no file in `--libs` or `--modules`, including dependencies with zero imports. These do not increase the import or reference counts. Windows can skip a missing dependency when no import needs it; Linux still needs the file. The audit also lists imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. +The audit also walks the built libraries' own host dependencies: each `.prx` under `--libs` has its PE imports (or ELF `DT_NEEDED` entries) checked for `.prx` names with no file in `--libs` or `--modules`, following present libraries transitively. Gaps fail the audit and are reported as `Importer: Chain: -> ... -> ` lines, since the loader fails on them even when every game import resolves. + | Option | Effect | |--------|--------| | `--libs ` | Built `.prx` directory. Required; repeat for several. | diff --git a/tools/build_web.py b/tools/build_web.py index 0d8bb92408..7af1cc6992 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -9,6 +9,7 @@ import urllib.request import zipfile ROOT = Path(__file__).resolve().parent.parent +MAX_EXPANDED_BYTES = 512 * 1024 * 1024 def copy_sources(output): @@ -70,10 +71,12 @@ def publish_libraries(output): if name in names: raise ValueError(f"Duplicate library archive path: {file.filename}") names.add(name) + expanded = sum(file.file_size for file in files) + if expanded > MAX_EXPANDED_BYTES: + raise ValueError("Release archive exceeds maximum expanded size") 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} diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..ca1c0a5bf0 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -10,7 +10,7 @@ from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") DROP = ("-c", "-MD", "-MMD", "-MP") -DROP_WITH_VALUE = ("-o", "-MT", "-MF") +DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MJ") def capture(command, cwd=None): @@ -34,26 +34,28 @@ def entries(build): ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") - result = capture([ninja, "-C", str(build), "-t", "compdb"]) + result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"]) if result.returncode != 0: raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") return json.loads(result.stdout) -def entry_key(entry): +def entry_key(entry, root): directory = Path(entry["directory"]).resolve() source = Path(entry["file"]) if not source.is_absolute(): source = directory / source source = source.resolve() - common = Path(os.path.commonpath([str(directory), str(source)])) - return os.path.relpath(source, common).replace(os.sep, "/") + try: + return source.relative_to(Path(root).resolve()).as_posix() + except ValueError: + return source.as_posix() -def index(build): +def index(build, root): commands = {} for entry in entries(build): - commands.setdefault(entry_key(entry), entry) + commands.setdefault(entry_key(entry, root), []).append(entry) return commands @@ -68,9 +70,36 @@ def changed(root, base): return [name for name in names if name.endswith(CPP)] +def command_arguments(command): + if os.name != "nt": + return shlex.split(command) + import ctypes + from ctypes import wintypes + count = ctypes.c_int() + split = ctypes.WinDLL("shell32", use_last_error=True).CommandLineToArgvW + split.argtypes = (wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)) + split.restype = ctypes.POINTER(wintypes.LPWSTR) + free = ctypes.WinDLL("kernel32", use_last_error=True).LocalFree + free.argtypes = (wintypes.HLOCAL,) + free.restype = wintypes.HLOCAL + arguments = split(command, ctypes.byref(count)) + if not arguments: + raise ctypes.WinError(ctypes.get_last_error()) + try: + return [arguments[index] for index in range(count.value)] + finally: + free(arguments) + + def syntax_only(entry, source): - arguments = shlex.split(entry["command"]) + arguments = command_arguments(entry["command"]) directory = Path(entry["directory"]) + return syntax_arguments(arguments, directory, source) + + +def syntax_arguments(arguments, directory, source): + if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: + raise ValueError("arguments must be a nonempty list of strings with an executable") wanted = source.resolve() trimmed = [] index = 0 @@ -79,7 +108,7 @@ def syntax_only(entry, source): if argument in DROP_WITH_VALUE: index += 2 continue - if argument in DROP: + if argument in DROP or argument.startswith("-MJ"): index += 1 continue if not argument.startswith("-"): @@ -96,24 +125,29 @@ def syntax_only(entry, source): def check(root, build, files): - commands = index(build) + commands = index(build, root) checked = 0 failed = 0 for file in files: source = file.resolve() name = os.path.relpath(source, root).replace(os.sep, "/") - entry = commands.get(name) - if entry is None: + configurations = commands.get(name) + if configurations is None: print(f"{name}: no compile command in {build}", file=sys.stderr) continue checked += 1 - result = subprocess.run(syntax_only(entry, source), cwd=entry["directory"], - capture_output=True, text=True, encoding="utf-8", errors="replace") - if result.returncode == 0: - continue - failed += 1 - output = (result.stdout or "") + (result.stderr or "") - sys.stderr.write(output if output.endswith("\n") else output + "\n") + source_failed = False + for entry in configurations: + command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) + if "arguments" in entry else syntax_only(entry, source)) + result = subprocess.run(command, cwd=entry["directory"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0: + continue + source_failed = True + output = (result.stdout or "") + (result.stderr or "") + sys.stderr.write(output if output.endswith("\n") else output + "\n") + failed += int(source_failed) return checked, failed diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..2d62d2dce1 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -20,7 +20,7 @@ RULES = { "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), - "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), + "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository; only the root AGENTS.md is allowed"), "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), "ci-gate": ("error", "CONTRIBUTING.md#build-and-test", "A job that runs for pull requests needs a file filter: paths on the pull_request trigger, or needs eligibility and an if on its outputs"), "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), @@ -50,7 +50,7 @@ SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") -FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) +FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*\r?$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") @@ -170,7 +170,7 @@ class Check: if not exports: return pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" - result = subprocess.run(["git", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") + result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") hits = [line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))] self.read(path for path, _, _ in hits) for path, number, text in hits: @@ -372,7 +372,9 @@ def jobs(lines, source): for index, (number, indent, name, _) in enumerate(entries): if indent != entries[0][1]: continue - fields = {entry[2]: field(source, entry) for entry in block(entries, index) if entry[1] == indent + 2} + children = block(entries, index) + child = min((entry[1] for entry in children), default=indent + 2) + fields = {entry[2]: field(source, entry) for entry in children if entry[1] == child} result.append((number, name, re.findall(r"[A-Za-z0-9_-]+", fields.get("needs", "")), fields.get("if", ""))) return result @@ -393,7 +395,7 @@ def library(path): def allowed_notes(path): p = PurePosixPath(path) if p.name.lower() in AGENT_FILES: - return False + return path == "AGENTS.md" if p.suffix.lower() not in NOTES: return None return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) diff --git a/tools/check_skips.py b/tools/check_skips.py index af4db8a61c..169cc7bc31 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -50,7 +50,7 @@ if __name__ == "__main__": parser.error(str(error)) if args.list: for name, output in found: - reason = next((line.strip() for line in output.splitlines() if line.startswith("skipped")), "") + reason = next((line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped")), "") print(f"{name}: {reason}") raise SystemExit if args.job == "windows": diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 6a4b16d4c4..da156692b5 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -94,21 +94,24 @@ def registered(build): if directory in visited or not file.is_file(): continue visited.add(directory) - tests, disabled = [], {} + tests, latest = [], {} for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): if command == "subdirs": pending.extend(directory / argument for argument in arguments) elif command == "add_test" and arguments: - tests.append((arguments[0], arguments[1:])) + test = [arguments[1:], False] + tests.append(test) + latest[arguments[0]] = test elif command == "set_tests_properties" and "PROPERTIES" in arguments: properties = arguments.index("PROPERTIES") values = arguments[properties + 1:] for key, value in zip(values[::2], values[1::2]): if key == "DISABLED": for name in arguments[:properties]: - disabled[name] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") - for test, arguments in tests: - if disabled.get(test, False): + if name in latest: + latest[name][1] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") + for arguments, disabled in tests: + if disabled: continue for index, argument in enumerate(arguments): if index and (argument.startswith("-") or re.match(r"^[A-Za-z_]\w*=", argument)): diff --git a/tools/compile_slot.sh b/tools/compile_slot.sh new file mode 100755 index 0000000000..2f01329de4 --- /dev/null +++ b/tools/compile_slot.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -u +dir=${ANYPS5_COMPILE_SLOT_DIR:?} +slots=${ANYPS5_COMPILE_SLOTS:?} +while :; do + for ((i = 0; i < slots; i++)); do + [ -e "$dir/slot-$i" ] || : >> "$dir/slot-$i" + exec {fd}<"$dir/slot-$i" + if flock -n "$fd"; then + exec "$@" + fi + exec {fd}>&- + done + sleep 0.1 +done diff --git a/tools/configure_seed.py b/tools/configure_seed.py new file mode 100644 index 0000000000..071d48e1a1 --- /dev/null +++ b/tools/configure_seed.py @@ -0,0 +1,40 @@ +import argparse +import re +import sys +from pathlib import Path + +CHECK = re.compile(r"^//(Have (include|includes|function|symbol|library) |Test |Result of (TRY_COMPILE|TRY_RUN|TEST_BIG_ENDIAN|check_))") +ENTRY = re.compile(r"^([^#/:][^:]*):([A-Z]+)=(.*)$") + + +def checks(cache): + found, doc = {}, "" + for line in Path(cache).read_text(encoding="utf-8").splitlines(): + if line.startswith("//"): + doc = line + continue + match = ENTRY.match(line) + if match and match.group(2) == "INTERNAL" and CHECK.match(doc): + found[match.group(1)] = (match.group(3), doc[2:]) + doc = "" + return found + + +def quote(value): + return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + '"' + + +def main(): + parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") + parser.add_argument("cache", help="CMakeCache.txt of a configured tree") + parser.add_argument("seed", help="file to pass to cmake -C") + args = parser.parse_args() + found = checks(args.cache) + Path(args.seed).write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") + print(f"{len(found)} check results") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/debt_split.py b/tools/debt_split.py index 0a425396f6..6f8bfc054c 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -46,7 +46,7 @@ def key(kind, text): def relink(text, source, destination): def replace(match): target = match.group(1) - if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:|//", target): + if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:|/", target): return match.group(0) path, _, anchor = target.partition("#") resolved = posixpath.normpath(posixpath.join(posixpath.dirname(source), path)) if path else source diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 8424ab09bf..b81f53d966 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -65,12 +65,18 @@ def oracle(): @cache -def detected_target(binary): - return subprocess.run([binary, "--target"], capture_output=True, text=True, check=True).stdout.strip() +def detected_target(binary, visible_devices): + environment = dict(os.environ) + if visible_devices is None: + environment.pop("ROCR_VISIBLE_DEVICES", None) + else: + environment["ROCR_VISIBLE_DEVICES"] = visible_devices + return subprocess.run([binary, "--target"], capture_output=True, text=True, check=True, + env=environment).stdout.strip() def target(): - return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle()) + return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle(), os.environ.get("ROCR_VISIBLE_DEVICES")) def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -120,8 +126,12 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de for start in range(0, len(padded), rows_per_dispatch): chunk = padded[start:start + rows_per_dispatch] (work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra) + (work / "out.bin").unlink(missing_ok=True) subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env, stdout=sys.stderr) data = (work / "out.bin").read_bytes() + expected = len(chunk) * 64 + if len(data) != expected: + raise ValueError(f"Oracle output size: expected {expected} bytes, got {len(data)}") out += [struct.unpack_from("<16I", data, lane * 64) for lane in range(len(chunk))] return out[:len(rows)] @@ -152,6 +162,8 @@ def main(): parser.add_argument("--json", action="store_true", help="emit inputs, results and run settings as JSON; target is the assembly target, not a GPU model or driver") args = parser.parse_args() rows = [tuple(parse_word(v) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] + if not rows: + parser.error("rows file must contain at least one input row") 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 31fa5240e0..a3321dc3e1 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -79,6 +79,35 @@ class OracleCacheTests(unittest.TestCase): class InputRowsTests(unittest.TestCase): + def test_successful_dispatch_without_output_cannot_reuse_previous_results(self): + with tempfile.TemporaryDirectory() as directory: + runner = Path(directory) / "runner.py" + runner.write_text( + "import sys\nfrom pathlib import Path\n" + "output = Path(sys.argv[3])\n" + "marker = output.with_suffix('.seen')\n" + "if not marker.exists():\n" + " output.write_bytes(bytes(int(sys.argv[1]) * 64))\n" + " marker.touch()\n", encoding="utf-8") + for wave64 in (False, True): + with self.subTest(wave64=wave64), patch.object(hw_oracle, "assemble", return_value=runner), \ + patch.object(hw_oracle, "oracle", return_value=sys.executable): + with self.assertRaises(FileNotFoundError): + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)] * 1025, wave64=wave64, **MODES) + + + def test_oracle_output_must_match_the_padded_dispatch_size(self): + for delta in (-1, 1, 64): + def dispatch(command, **kwargs): + Path(command[4]).write_bytes(bytes(int(command[2]) * 64 + delta)) + + with self.subTest(delta=delta), 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): + with self.assertRaisesRegex(ValueError, "Oracle output size"): + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)], **MODES) + + 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)): @@ -87,6 +116,28 @@ class InputRowsTests(unittest.TestCase): hw_oracle.run("s_nop 0", [row], **MODES) assemble.assert_not_called() + def test_cli_rejects_empty_rows_before_running_tools(self): + with tempfile.TemporaryDirectory() as tmp: + body, rows = Path(tmp) / "body.s", Path(tmp) / "rows.txt" + body.write_text("s_nop 0", encoding="utf-8") + argv = ["hw_oracle.py", str(body), str(rows), "--json"] + argv += [part for name, value in MODES.items() + for part in ["--" + name.replace("_", "-"), str(value)]] + for contents in ("", "\n \t\n"): + with self.subTest(contents=contents): + rows.write_text(contents, encoding="utf-8") + with patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[]) as run, \ + patch.object(hw_oracle, "target", return_value="gfx1030") as target, \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()) as errors, \ + self.assertRaises(SystemExit) as exited: + hw_oracle.main() + self.assertEqual(exited.exception.code, 2) + self.assertIn("at least one input row", errors.getvalue()) + self.assertEqual(output.getvalue(), "") + run.assert_not_called() + target.assert_not_called() + def test_unsigned_bits_and_indexable_values_are_preserved(self): class Indexable: def __index__(self): @@ -279,6 +330,39 @@ class TargetOverrideTests(unittest.TestCase): with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): self.assertEqual(hw_oracle.target(), "gfx1100") + def test_device_selection_uses_its_own_cached_target(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runner = root / "probe.py" + runner.write_text( + "import os\nfrom pathlib import Path\n" + "visible = os.environ.get('ROCR_VISIBLE_DEVICES', 'unset')\n" + "with Path(__file__).with_name('probes.txt').open('a', encoding='utf-8') as log:\n" + " log.write(visible + '\\n')\n" + "print({'0': 'gfx1030', '1': 'gfx1100', 'unset': 'gfx900'}[visible])\n", + encoding="utf-8") + binary = root / ("oracle.cmd" if os.name == "nt" else "oracle") + if os.name == "nt": + binary.write_text(f'@"{sys.executable}" "{runner}" %*\n', encoding="utf-8") + else: + binary.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(runner))} "$@"\n', + encoding="utf-8") + binary.chmod(0o755) + with patch.object(hw_oracle, "oracle", return_value=binary), \ + patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + os.environ["ROCR_VISIBLE_DEVICES"] = "0" + self.assertEqual(hw_oracle.detected_target(binary, "1"), "gfx1100") + for visible, expected in (("0", "gfx1030"), ("0", "gfx1030"), + ("1", "gfx1100"), ("1", "gfx1100"), + ("0", "gfx1030"), (None, "gfx900"), (None, "gfx900")): + if visible is None: + os.environ.pop("ROCR_VISIBLE_DEVICES", None) + else: + os.environ["ROCR_VISIBLE_DEVICES"] = visible + self.assertEqual(hw_oracle.target(), expected) + self.assertEqual((root / "probes.txt").read_text(encoding="utf-8").splitlines(), + ["1", "0", "unset"]) + 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")), \ diff --git a/tools/import_audit.py b/tools/import_audit.py index 037dba8fe3..704b1ec45a 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -182,7 +182,7 @@ def elf_exports(path, data): binding, visibility = info >> 4, other & 3 if name == 0 or index == 0 or binding not in (1, 2, 10) or visibility not in (0, 3): continue - names.add(cstring(data, strings_offset + name, strings_offset + strings_size, path, "export name")) + names.add(cstring(data, strings_offset + name, strings_offset + strings_size, path, "export name").split(MODULE_SUFFIX, 1)[0]) return names @@ -220,7 +220,7 @@ def elf_imports(path, data): return symbols, needed -def pe_exports(path, data): +def pe_headers(path, data): header = unpack("6} imports {summary['references_by_class'][kind]:>6} references") @@ -587,6 +672,10 @@ def render(summary, records, missing, stops): lines.extend(f" {library}: {count} imports" for library, count in missing.items()) if any(count == 0 for count in missing.values()): lines.append(" Libraries with 0 imports are declared dependencies; Windows can skip them if no import needs them") + if missing_hosts: + lines.append("") + lines.append(f"Host dependencies with no file in --libs or --modules ({len(missing_hosts)})") + lines.extend(f" Importer: {entry['importer']} Chain: {' -> '.join(entry['chain'])}" for entry in missing_hosts) if summary["library_mismatch"]: lines.append("") lines.append(f"{summary['library_mismatch']} imports are exported only by a library other than the one they name: they resolve on Linux and can fail on Windows") @@ -618,11 +707,15 @@ def main(argv=None): imports = [entry for _, entries in registries for entry in entries] needed = sorted({library for path in dependency_registries for library in read_needed_registry(path)}) exports, libraries = built_libraries(args.libs) + host_dependencies = built_host_dependencies(args.libs) 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, needed_libraries=needed) + modules = set(module_imports) + modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} + records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) + missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules) if args.names: attach_names(records, load_names(args.names)) - summary = summarize(records, sum(record["references"] for record in records), missing) + summary = summarize(records, sum(record["references"] for record in records), missing, missing_hosts) stops = [{"registry": str(path), "stop": current_stop(entries, records)} for path, entries in registries] if args.json: commit, dirty = git_stamp(Path(args.source)) @@ -638,8 +731,8 @@ def main(argv=None): except OSError as error: print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 - print(render(summary, records, missing, stops)) - return 1 if summary["unique_by_class"]["absent"] or missing or (args.strict_providers and summary["library_mismatch"]) else 0 + print(render(summary, records, missing, stops, missing_hosts)) + return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) else 0 if __name__ == "__main__": diff --git a/tools/nid_names.py b/tools/nid_names.py index 2c4cce5a23..82292e7e85 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -26,6 +26,7 @@ CPP_TOKEN = re.compile( r"|(?P(?:[0-9]|\.[0-9])[A-Za-z0-9_.']*|[A-Za-z_][A-Za-z0-9_]*|\S)") DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" +ASM_FUNCTION_MACROS = {"APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION"} def compute_nid(symbol): @@ -92,6 +93,22 @@ def source_definitions(text): return DEFINITION.finditer(CPP_TOKEN.sub(mask, text.replace("\\\n", "").replace("\\\r\n", ""))) +def source_assembly_definitions(text): + tokens = [match for match in CPP_TOKEN.finditer(text.replace("\\\n", "").replace("\\\r\n", "")) + if match.group("comment") is None] + for index, token in enumerate(tokens): + if token.group("code") is None or token.group() not in ASM_FUNCTION_MACROS: + continue + if index + 3 >= len(tokens): + continue + opening, literal, closing = tokens[index + 1:index + 4] + if opening.group() != "(" or closing.group() != ")" or literal.group("string") is None: + continue + match = re.fullmatch(r'"([A-Za-z_][A-Za-z0-9_]*)"', literal.group("string")) + if match is not None: + yield match.group(1) + + def collect_unknowns(): found = {} for path in source_files(PRX): @@ -111,6 +128,10 @@ def collect_real_names(): names.add(name) if name.endswith(NID_POSTFIX): names.add(name[:-len(NID_POSTFIX)]) + for name in source_assembly_definitions(text): + names.add(name) + if name.endswith(NID_POSTFIX): + names.add(name[:-len(NID_POSTFIX)]) return names @@ -167,8 +188,8 @@ def main(argv=None): requested_nids = "--nid" in requested_options if requested_nids and requested_options & {"--name", "--symbol"}: parser.error("--name/--symbol cannot be combined with --nid") - if args.name: - rows = [{"name": name, "nid": compute_nid(name)} for name in args.name] + if requested_options & {"--name", "--symbol"}: + rows = [{"name": name, "nid": compute_nid(name)} for name in (args.name or [])] if args.json: print(json.dumps({"symbols": rows}, indent=1)) else: @@ -221,4 +242,7 @@ def main(argv=None): if __name__ == "__main__": + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") sys.exit(main()) diff --git a/tools/package_release.py b/tools/package_release.py index 52f5185279..20daabb45e 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -60,8 +60,12 @@ def package(platform, build, output, version): destination = output / name if destination.exists() and not destination.is_file(): raise RuntimeError(f"Release destination is not a file: {destination}") - if os.name == "nt" and os.path.lexists(destination) and destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: - raise RuntimeError(f"Release destination is read-only: {destination}") + if os.name == "nt" and os.path.lexists(destination): + attributes = destination.lstat().st_file_attributes + if attributes & stat.FILE_ATTRIBUTE_DIRECTORY: + raise RuntimeError(f"Release destination is not a file: {destination}") + if attributes & stat.FILE_ATTRIBUTE_READONLY: + raise RuntimeError(f"Release destination is read-only: {destination}") output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: staging = Path(temporary) diff --git a/tools/pad_input.py b/tools/pad_input.py index 38522df145..a83c7a1116 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -12,7 +12,8 @@ BUTTONS = { "up": 0x10, "right": 0x20, "down": 0x40, "left": 0x80, "l2": 0x100, "r2": 0x200, "l1": 0x400, "r1": 0x800, "triangle": 0x1000, "circle": 0x2000, "cross": 0x4000, - "square": 0x8000, "touchpad": 0x100000, "release": 0, + "square": 0x8000, "touchpad": 0x100000, + "touch-left": 0x200000, "touch-right": 0x400000, "release": 0, } @@ -32,7 +33,7 @@ def previous_sequence(path): values = [int(token) for token in tokens] if any(value > 2**64 - 1 for value in values): return 0 - if values[0] == 0 or values[1] & ~0x10fffe or not 1 <= values[2] <= 5000: + if values[0] == 0 or values[1] & ~0x70fffe or not 1 <= values[2] <= 5000: return 0 if any(axis > 255 for axis in values[3:]): return 0 diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 31131859f9..bbbf7c949d 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -57,7 +57,7 @@ def validate(summary): def load_summary(path, trace=None): records = [] - with Path(path).open(encoding="utf-8") as source: + with Path(path).open(encoding="utf-8-sig") as source: for line_number, line in enumerate(source, 1): if not line.strip(): continue diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index b82912cd76..a7e2f394fa 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,9 +3,10 @@ import json import os import re import subprocess +from concurrent.futures import ThreadPoolExecutor from itertools import combinations from pathlib import Path -from urllib.parse import urlencode +from urllib.parse import quote, urlencode PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") @@ -21,8 +22,8 @@ def git(*args): 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} + ancestors = set(git("rev-list", sha).splitlines()) + return {n: pr for n, pr in prs.items() if pr["sha"] not in ancestors} def gh(*args): @@ -124,6 +125,11 @@ def scan(pr): pr["exports"] = exports(diff) +def scan_all(prs, jobs): + with ThreadPoolExecutor(max_workers=jobs) as executor: + list(executor.map(scan, prs.values())) + + def hunks(pr, path): diff = git("diff", "-U0", "--no-color", "refs/pr/base", pr["tree"], "--", path) found = [] @@ -184,10 +190,14 @@ def cell(items): def spans(conflicts, blob): - return "
".join(f"`{path}` " + ", ".join(f"[L{s}-{e}]({blob}/{path}#L{s}-L{e})" if s != e else f"[L{s}]({blob}/{path}#L{s})" + return "
".join(f"`{path}` " + ", ".join(f"[L{s}-{e}]({blob}/{quote(path)}#L{s}-L{e})" if s != e else f"[L{s}]({blob}/{quote(path)}#L{s})" for s, e in ranges) for path, ranges in sorted((conflicts or {}).items())) +def pull(blob, number): + return f'[PR {number}]({blob.split("/blob/", 1)[0].replace("https://github.com/", "https://www.github.com/", 1)}/pull/{number})' + + def report(rows, blob): if not rows: return "" @@ -204,7 +214,7 @@ def report(rows, blob): "|---|---|---|---|", ]) for number, found in sorted(confirmed.items()): - lines.append(f'| #{number} | {spans(found.get("conflicts"), blob)} | {cell(found.get("exports"))} | {cell(found["added"])} |') + lines.append(f'| {pull(blob, number)} | {spans(found.get("conflicts"), blob)} | {cell(found.get("exports"))} | {cell(found["added"])} |') if uncompared: if confirmed: lines.append("") @@ -216,7 +226,7 @@ def report(rows, blob): "|---|---|", ]) for number, files in sorted(uncompared.items()): - lines.append(f"| #{number} | {cell(files)} |") + lines.append(f"| {pull(blob, number)} | {cell(files)} |") return "\n".join(lines) + "\n" @@ -232,9 +242,12 @@ if __name__ == "__main__": parser.add_argument("--out", type=Path, help="write .md for every open pull request (empty when clean)") parser.add_argument("--branch", help="check a local branch against the open pull requests instead") parser.add_argument("--repo", help="owner/name; defaults to the upstream of the current checkout") + parser.add_argument("--jobs", type=int, default=4, help="number of pull request comparisons to run at once") args = parser.parse_args() if bool(args.out) == bool(args.branch): parser.error("pass exactly one of --out or --branch") + if args.jobs < 1: + parser.error("--jobs must be at least 1") REPO = args.repo or upstream() prs = open_prs(args.base) fetch(args.base, prs) @@ -242,8 +255,7 @@ if __name__ == "__main__": sha = git("rev-parse", args.branch).strip() prs = own(prs, sha) prs[0] = {"ref": sha, "author": None, "depends": set()} - for pr in prs.values(): - scan(pr) + scan_all(prs, args.jobs) rows = {n: {} for n in prs} pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) for a, b in pairs: diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index ed4c2e5562..63119d81a1 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -19,12 +19,17 @@ def release_body(notes): def prepare_notes(source, body, full): + paths = (source, body, full) + for index, first in enumerate(paths): + for second in paths[index + 1:]: + if first.resolve() == second.resolve() or (first.exists() and second.exists() and first.samefile(second)): + raise ValueError("Release note input and outputs must be distinct files") notes = json.loads(source.read_text(encoding="utf-8"))["body"] if not isinstance(notes, str): raise ValueError("Generated release notes body must be a string") full.parent.mkdir(parents=True, exist_ok=True) - full.write_text(notes, encoding="utf-8", newline="") - body.write_text(release_body(notes), encoding="utf-8", newline="") + full.write_bytes(notes.encode("utf-8")) + body.write_bytes(release_body(notes).encode("utf-8")) def main(): diff --git a/tools/progress.py b/tools/progress.py index 8a4f5995f3..cc58eb0ede 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -306,7 +306,8 @@ def treemap(title, data, left): for i, cell in enumerate(cells(total, x + 1, y + 1, w - 2, h - 2)): parts.append(rect(*cell, DONE_COLOR if i < group["done"] else TODO_COLOR)) parts.append(f'') - label = group["label"][:int((w - 10) / 7)] + label_length = int((w - 10) / 7) + label = group["label"][:label_length] if label_length > 0 else "" if (label == group["label"] or len(label) >= 3) and h > 22: parts.append(text(x + 5, y + 16, label, 12)) parts.append("") diff --git a/tools/select_tests.py b/tools/select_tests.py new file mode 100644 index 0000000000..5def841494 --- /dev/null +++ b/tools/select_tests.py @@ -0,0 +1,252 @@ +import argparse +import collections +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +FULL = re.compile(r"^CMakeLists\.txt$|^core/CMakeLists\.txt$|^core/libs/CMakeLists\.txt$|^\.github/workflows/build\.yml$" + r"|^3rdparty/|^cmake/|^\.gitmodules$|^tools/select_tests\.py$") +LOCAL = re.compile(r"(^|/)CMakeLists\.txt$|\.cmake$") +BATCH = 20000 + + +class Build: + def __init__(self, path): + cache = {} + for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + name, _, value = line.partition("=") + cache[name.partition(":")[0]] = value + self.path = os.path.normpath(cache["CMAKE_CACHEFILE_DIR"]) + self.source = os.path.normpath(cache["CMAKE_HOME_DIRECTORY"]) + self.program = cache.get("CMAKE_MAKE_PROGRAM") or "ninja" + self.encoding = "utf-8" + if sys.platform == "win32": + codepage = self.run("-t", "wincodepage").strip() + self.encoding = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"}[codepage] + + def run(self, *args): + return subprocess.run([self.program, "-C", self.path, *args], capture_output=True, text=True, + encoding=self.encoding, check=True).stdout + + def absolute(self, path): + return os.path.normpath(path if os.path.isabs(path) else os.path.join(self.path, path)) + + def key(self, path): + return os.path.normcase(self.absolute(path)) + + def repo(self, path): + absolute = self.absolute(path) + if inside(absolute, self.path) or not inside(absolute, self.source): + return None + return relative(absolute, self.source) + + def query(self, targets): + found = {} + for batch in chunks(targets): + current, section = None, None + for line in self.run("-t", "query", *batch).splitlines(): + if not line.startswith(" "): + current = line.rstrip(":") + found[current] = {"inputs": [], "phony": False} + elif line.startswith(" input: "): + section = "inputs" + found[current]["phony"] = line.strip() == "input: phony" + elif line.startswith(" outputs:"): + section = None + elif section and line.startswith(" "): + found[current]["inputs"].append(line.strip().removeprefix("|| ").removeprefix("| ")) + return found + + def walk(self, roots): + nodes, todo = {}, list(roots) + while todo: + batch = sorted({t for t in todo if t not in nodes}) + if not batch: + break + nodes.update(self.query(batch)) + todo = [i for t in batch for i in nodes.get(t, {"inputs": []})["inputs"]] + return nodes + + def leaves(self): + nodes = self.walk(["all"]) + leaves, todo, seen = set(), ["all"], set() + while todo: + name = todo.pop() + if name in seen or name not in nodes: + continue + seen.add(name) + if nodes[name]["phony"]: + todo.extend(nodes[name]["inputs"]) + else: + leaves.add(name) + return sorted(leaves) + + def tests(self): + listing = json.loads(subprocess.run(["ctest", "--test-dir", self.path, "--show-only=json-v1"], + capture_output=True, text=True, check=True).stdout) + return listing, listing["tests"] + + def output(self, path): + absolute = self.absolute(path) + if not inside(absolute, self.path): + return None + return relative(absolute, self.path) + + def target(self, test): + command = test.get("command") or [] + return self.output(command[0]) if command else None + + +def inside(path, root): + prefix = os.path.normcase(root).rstrip(os.sep) + os.sep + return os.path.normcase(path).startswith(prefix) + + +def relative(path, root): + root_len = len(root.rstrip(os.sep)) + return path[root_len + 1:].replace(os.sep, "/") + + +def chunks(names): + batch, size = [], 0 + for name in names: + if batch and size + len(name) + 1 > BATCH: + yield batch + batch, size = [], 0 + batch.append(name) + size += len(name) + 1 + if batch: + yield batch + + +def dump(build, out): + deps = collections.defaultdict(set) + current = None + for line in build.run("-t", "deps").splitlines(): + if not line.startswith(" ") and ": #deps" in line: + current = build.key(line.split(": #deps")[0]) + elif line.strip() and current: + path = build.repo(line.strip()) + if path: + deps[current].add(path) + listing, listed = build.tests() + graph = listing["backtraceGraph"] + targets = {build.target(t) for t in listed} - {None} + nodes = build.walk(sorted(targets)) + closures = {} + for target in sorted(targets): + files, seen, todo = set(), set(), [target] + while todo: + name = todo.pop() + if name in seen: + continue + seen.add(name) + path = build.repo(name) + if path: + files.add(path) + files |= deps.get(build.key(name), set()) + todo.extend(nodes.get(name, {"inputs": []})["inputs"]) + closures[target] = sorted(files) + tests = {} + for test in listed: + registered, node = set(), test.get("backtrace") + while node is not None: + registered.add(build.repo(graph["files"][graph["nodes"][node]["file"]])) + node = graph["nodes"][node].get("parent") + props = {p["name"]: p["value"] for p in test.get("properties", [])} + arguments = sorted({build.repo(a) for a in (test.get("command") or [])[1:] if os.path.isabs(a)} - {None}) + tests[test["name"]] = {"registered": sorted(registered - {None}), "arguments": arguments, + "requires": props.get("FIXTURES_REQUIRED", []), "sets": props.get("FIXTURES_SETUP", []), + "depends": props.get("DEPENDS", []), "target": build.target(test)} + with open(out, "w", encoding="utf-8") as file: + json.dump({"tests": tests, "closures": closures}, file) + print(f"{len(tests)} tests, {len(closures)} test executables, {len(deps)} objects with header deps") + + +def select(graph, changed): + full = [f for f in changed if FULL.search(f)] + if full: + return None, full + tests, closures = graph["tests"], {t: set(f) for t, f in graph["closures"].items()} + known = set().union(*closures.values(), *(t["arguments"] for t in tests.values())) + files = set(changed) + dirs = tuple(f.rpartition("/")[0] + "/" for f in changed if "/" in f and (LOCAL.search(f) or f not in known)) + picked = set() + for name, test in tests.items(): + scope = closures.get(test["target"], set()) | set(test["arguments"]) + if (test["target"] is None or files & set(test["registered"]) or files & scope + or any(f.startswith(dirs) for f in scope | set(test["registered"]))): + picked.add(name) + setups = collections.defaultdict(set) + for name, test in tests.items(): + for fixture in test["sets"]: + setups[fixture].add(name) + todo = list(picked) + while todo: + test = tests[todo.pop()] + for extra in set(test["depends"]).union(*(setups[f] for f in test["requires"])): + if extra in tests and extra not in picked: + picked.add(extra) + todo.append(extra) + return picked, [] + + +def plan(build, graph, picked): + _, listed = build.tests() + run = [i for i, t in enumerate(listed, 1) if t["name"] in picked or t["name"] not in graph["tests"]] + names = {listed[i - 1]["name"] for i in run} + keep = {build.output(a) for t in listed if t["name"] in names for a in t.get("command") or [] if os.path.isabs(a)} + skip = {build.target(t) for t in listed if t["name"] not in names} - keep - {None} + return run, [t for t in build.leaves() if t not in skip], len(listed) + + +def escape(name): + return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ") + + +def main(): + parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") + sub = parser.add_subparsers(dest="command", required=True) + d = sub.add_parser("dump", help="write the test graph of a built tree") + d.add_argument("build") + d.add_argument("graph") + s = sub.add_parser("select", help="write a ninja file and a ctest index list for a configured tree") + s.add_argument("build") + s.add_argument("graph") + s.add_argument("changed", help="file with one changed path per line") + c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") + c.add_argument("build") + c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") + args = parser.parse_args() + if args.command == "ctest": + selected = Path(args.build, "select.tests") + extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] + return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode + build = Build(args.build) + if args.command == "dump": + dump(build, args.graph) + return 0 + for name in ("select.ninja", "select.tests"): + Path(build.path, name).unlink(missing_ok=True) + if not os.path.exists(args.graph): + print("full run: no test graph") + return 0 + graph = json.loads(Path(args.graph).read_text(encoding="utf-8")) + changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8").splitlines() if line.strip()] + picked, full = select(graph, changed) + if picked is None: + print(f"full run: {', '.join(full[:5])}") + return 0 + run, targets, total = plan(build, graph, picked) + Path(build.path, "select.ninja").write_text("include build.ninja\nbuild selected: phony " + + " ".join(escape(t) for t in targets) + "\n", encoding=build.encoding) + Path(build.path, "select.tests").write_text("0,0,0," + ",".join(map(str, run)) + "\n", encoding="utf-8") + print(f"selected {len(run)}/{total} tests, {len(targets)} build targets") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_build_eligibility.py b/tools/tests/test_build_eligibility.py index b39ec9fabe..56e41b71c6 100644 --- a/tools/tests/test_build_eligibility.py +++ b/tools/tests/test_build_eligibility.py @@ -94,7 +94,7 @@ class RelinkerEligibilityTests(unittest.TestCase): encoding="utf-8") environment = dict(os.environ, PR_NUMBER="7", GITHUB_TRIGGERING_ACTOR="maintainer") result = subprocess.run([self.node, "harness.js", "scenario.json", "gate.js"], cwd=self.root, - capture_output=True, text=True, env=environment, timeout=20) + capture_output=True, text=True, env=environment, timeout=90) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) return json.loads(result.stdout) diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 872c9f03eb..47c057babc 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -24,6 +24,24 @@ def library_zip(entries): class LibraryPublicationTests(unittest.TestCase): + def test_expanded_limit_is_checked_before_decompression(self): + payload = library_zip([("libs/libc.prx", b"a" * 4097)]) + with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True), \ + patch.object(zipfile.ZipFile, "testzip", side_effect=AssertionError("archive decompressed")): + with self.assertRaisesRegex(ValueError, "expanded size"): + self.publish(payload, payload) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + + def test_expanded_limit_boundary_can_be_published(self): + payload = library_zip([("libs/libc.prx", b"a" * 4096)]) + with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True): + self.publish(payload, payload) + report = json.loads(self.manifest.read_text(encoding="utf-8")) + self.assertTrue(all(asset["expandedBytes"] == 4096 for asset in report["platforms"].values())) + + def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..f4c22a0159 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -1,5 +1,6 @@ import json import os +import shutil import subprocess import sys import tempfile @@ -13,6 +14,39 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_ninja_response_file_is_expanded_before_syntax_rewriting(self): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None or shutil.which("ninja") is None: + self.skipTest("needs Ninja and a C compiler") + source = self.source("unit.c", "#ifndef REQUIRED\n#error missing definition\n#endif\nint x;\n") + command = subprocess.list2cmdline([compiler]) if os.name == "nt" else '"' + compiler + '"' + (self.build / "build.ninja").write_text( + "rule cc\n command = " + command + " @$out.rsp\n" + " rspfile = $out.rsp\n rspfile_content = -DREQUIRED -c $in -o $out -MD -MF $out.d\n" + "build unit.o: cc ../unit.c\n", encoding="utf-8") + self.assertFalse((self.build / "unit.o.rsp").exists()) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + self.assertFalse((self.build / "unit.o").exists()) + self.assertFalse((self.build / "unit.o.d").exists()) + self.assertFalse((self.build / "unit.o.rsp").exists()) + + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check compilation-fragment side effects") + source = self.source("unit.c") + fragment = self.build / "fragment.json" + for option in (f'-MJ "{fragment}"', f'-MJ"{fragment}"'): + with self.subTest(option=option): + fragment.write_bytes(b"previous compilation fragment") + self.manifest([self.entry("unit.c", f'"{compiler}" -c {option} "{source}"')]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(fragment.read_bytes(), b"previous compilation fragment") + + def setUp(self): holder = tempfile.TemporaryDirectory() self.addCleanup(holder.cleanup) @@ -80,6 +114,73 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + def test_every_configuration_is_checked_and_each_source_is_counted_once(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "with Path(sys.argv[1]).open('a') as log:\n" + " log.write(json.dumps([str(Path.cwd()), sys.argv[2:]]) + '\\n')\n" + "if sys.argv[2] == '1':\n" + " sys.stderr.write(sys.argv[3] + ': configuration failed\\n')\n" + "sys.exit(int(sys.argv[2]))\n") + seen = self.root / "invocations.jsonl" + second = self.build / "second" + second.mkdir() + for statuses in ((0, 0), (0, 1), (1, 0), (1, 1)): + with self.subTest(statuses=statuses): + seen.write_text("", encoding="utf-8") + first = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" ' + f'{statuses[0]} first -c -o first.o "{source}"') + other = {"directory": str(second), "file": os.path.relpath(source, second), + "arguments": [sys.executable, str(script), str(seen), + str(statuses[1]), "second", "-c", "-o", "second.o", + os.path.relpath(source, second)]} + self.manifest([first, other]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, int(any(statuses)), result.stderr) + invocations = [json.loads(line) for line in seen.read_text(encoding="utf-8").splitlines()] + self.assertEqual(invocations, [ + [str(self.build), [str(statuses[0]), "first", "-fsyntax-only", str(source)]], + [str(second), [str(statuses[1]), "second", "-fsyntax-only", str(source)]], + ]) + if any(statuses): + self.assertIn("1 of 1 translation units did not compile", result.stderr) + else: + self.assertIn("1 translation units compile", result.stdout) + for label, status in zip(("first", "second"), statuses): + self.assertEqual(f"{label}: configuration failed" in result.stderr, bool(status)) + + def test_a_later_configuration_with_a_real_compiler_error_fails(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check configuration-dependent compilation") + source = self.source("unit.c", "#ifdef SECOND_TARGET\n#error second target must fail\n#endif\nint x;\n") + self.manifest([ + {"directory": str(self.build), "file": str(source), + "arguments": [compiler, *flags, "-c", str(source)]} + for flags in ([], ["-DSECOND_TARGET"]) + ]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("second target must fail", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + + @unittest.skipUnless(os.name == "nt", "Windows command-line quoting") + def test_windows_paths_and_quoted_defines_survive_the_syntax_check(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + flags = ['-DNAME=quoted "value"', '-IC:\\sdk files\\include\\'] + arguments = [sys.executable, str(script), str(seen), *flags, + "-c", "-o", "unit.o", "-MD", "-MF", "unit.o.d", str(source)] + self.manifest([self.entry("unit.cpp", subprocess.list2cmdline(arguments))]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), + [*flags, "-fsyntax-only", str(source)]) + def test_nothing_checked_is_a_failure(self): self.manifest([]) result = self.tool("orphan.cpp") @@ -124,6 +225,24 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertIn("1 translation units compile", result.stdout) + def test_a_build_directory_beside_the_checkout_is_matched(self): + repo = self.root / "AnyPS5" + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + sibling = self.root / "sibling-build" + sibling.mkdir() + source = repo / "src" / "unit.cpp" + source.parent.mkdir() + source.write_text("int x;\n", encoding="utf-8") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = {"directory": str(sibling), "command": f'"{sys.executable}" "{script}" "{source}"', + "file": str(source)} + (sibling / "compile_commands.json").write_text(json.dumps([entry]), encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), "--build", str(sibling), "src/unit.cpp"], + cwd=repo, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("1 translation units compile", result.stdout) + def test_a_changed_file_with_a_non_ascii_name_is_checked(self): self.failing("caf\u00e9.cpp") subprocess.run(["git", "-C", str(self.root), "add", "caf\u00e9.cpp"], check=True) @@ -198,5 +317,37 @@ class CheckCompileTests(unittest.TestCase): self.assertIn("1 translation units compile", result.stdout) + def test_argument_arrays_preserve_tokens_and_take_precedence_over_command(self): + source = self.source("unit with spaces.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + flags = ['-DNAME=quoted "value"', '-IC:\\sdk files\\include\\'] + arguments = [sys.executable, str(script), str(seen), *flags, + "-c", "-o", "unit.o", "-MD", "-MT", "unit.o", "-MF", "unit.o.d", + os.path.relpath(source, self.build)] + entry = {"directory": str(self.build), "file": str(source), "arguments": arguments} + for command in (None, "nonexistent-compiler -c unit.cpp"): + with self.subTest(command=command): + if command is not None: + entry["command"] = command + self.manifest([entry]) + result = self.tool(source.name) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), + [*flags, "-fsyntax-only", str(source)]) + + def test_invalid_argument_arrays_are_rejected_without_a_traceback(self): + source = self.source("unit.cpp") + for arguments in (None, [], "compiler -c unit.cpp", ["compiler", 3], [""]): + with self.subTest(arguments=arguments): + self.manifest([{"directory": str(self.build), "file": str(source), "arguments": arguments}]) + result = self.tool(source.name) + self.assertEqual(result.returncode, 2) + self.assertIn("arguments must be a nonempty list of strings with an executable", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_conventions_agent_files.py b/tools/tests/test_check_conventions_agent_files.py index 87704e1226..fcef15671a 100644 --- a/tools/tests/test_check_conventions_agent_files.py +++ b/tools/tests/test_check_conventions_agent_files.py @@ -10,7 +10,7 @@ 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", + "CLAUDE.md", "claude.md", "Claude.md", "GEMINI.md", "Copilot-Instructions.md", ".CursorRules", ".WindsurfRules", "windsurf.md", "ChatGPT.md", ".ChatGPTRules", ".aider.conf.yml", ): @@ -18,6 +18,12 @@ class AgentFileTests(unittest.TestCase): self.assertIs(allowed_notes(f"docs/dev/{name}"), False) self.assertIs(allowed_notes(name), False) + def test_only_the_root_agents_file_is_allowed(self): + self.assertIs(allowed_notes("AGENTS.md"), True) + for path in ("agents.md", "Agents.md", "docs/dev/AGENTS.md", "core/AGENTS.md", "tools/agents.md"): + with self.subTest(path=path): + self.assertIs(allowed_notes(path), False) + def test_ordinary_docs_are_still_allowed(self): self.assertTrue(allowed_notes("docs/dev/Claudette.md")) self.assertTrue(allowed_notes("README.md")) diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py index 33f7d94501..200d32f39b 100644 --- a/tools/tests/test_check_conventions_ci_gate.py +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -92,6 +92,13 @@ class CiGateTests(unittest.TestCase): def test_other_triggers_pass(self): self.assertGated(workflow(" pull_request_target:\n types: [opened]\n push:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n")) + def test_four_space_indentation_is_read(self): + def wide(text): + return "\n".join(" " * (2 * (len(line) - len(line.lstrip(" ")))) + line.lstrip(" ") for line in text.split("\n")) + gated = " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n runs-on: ubuntu-latest\n" + self.assertGated(wide(workflow(PULL_REQUEST, gated))) + self.assertUngated(wide(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n")), "tests") + def test_repository_workflows_pass(self): files = {path.name: path.read_text(encoding="utf-8") for path in WORKFLOWS.glob("*.y*ml")} self.assertIn("build.yml", files) diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py index d230f88a30..3f847eae90 100644 --- a/tools/tests/test_check_conventions_doc_links.py +++ b/tools/tests/test_check_conventions_doc_links.py @@ -17,6 +17,10 @@ class DocLinkTests(unittest.TestCase): text = f"{FENCE}\n[](int a)\n{FENCE}\nsee [guide](../README.md)\n" self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + def test_real_link_after_crlf_code_block_is_kept(self): + text = f"{FENCE}\r\ncode\r\n{FENCE}\r\nsee [guide](../README.md)\r\n" + self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + def test_empty_link_text_is_ignored(self): self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py index 258fc2916a..c05d890dbb 100644 --- a/tools/tests/test_check_conventions_wrapped_declarations.py +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -47,6 +47,11 @@ 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_non_ascii_declaration_path_does_not_crash_duplicate_scan(self): + result = self.check("caf\u00e9.cpp", "int APS5_VABI sceFooBar(int a, int b);\n") + 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) diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 5ec9c41943..f56f9cc50a 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -152,6 +152,23 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("all expected", result.stdout) + def test_list_reports_skipped_reasons_including_indented_lines(self): + xml = ( + '' + '' + '\n skipped, host imports are unavailable\n' + '\n Skipped, no display\n' + '' + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--list"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("normal: skipped, device limitation", result.stdout) + self.assertIn("indented: skipped, host imports are unavailable", result.stdout) + self.assertIn("capitalized: Skipped, no display", result.stdout) + diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py new file mode 100644 index 0000000000..e1a1413fca --- /dev/null +++ b/tools/tests/test_configure_seed.py @@ -0,0 +1,94 @@ +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +TOOL = Path(__file__).resolve().parents[1] / "configure_seed.py" +sys.path.insert(0, str(TOOL.parent)) + +import configure_seed + +PROJECT = """cmake_minimum_required(VERSION 3.20) +project(mini C) +include(CheckIncludeFile) +include(CheckSymbolExists) +include(CheckCSourceCompiles) +check_include_file(stdio.h MINI_HAVE_STDIO_H) +check_include_file(mini_missing_header.h MINI_HAVE_MISSING_H) +check_symbol_exists(printf stdio.h MINI_HAVE_PRINTF) +check_c_source_compiles("int main(void) { return 0; }" MINI_COMPILES) +check_c_source_compiles("int main(void) { return undefined_name; }" MINI_FAILS) +set(MINI_OPTION "value" CACHE STRING "An option, not a check result") +""" + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class ConfigureSeedTests(unittest.TestCase): + def test_check_names_with_spaces_round_trip_through_cmake(self): + cache = self.root / "named-check.txt" + cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") + output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) + self.assertNotRegex(output, r"CMake Error") + seeded = self.root / "named-check" / "CMakeCache.txt" + self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") + self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) + + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + cls.root = Path(cls.directory.name) + (cls.root / "source").mkdir() + (cls.root / "source" / "CMakeLists.txt").write_text(PROJECT, encoding="utf-8") + cls.compiler = shutil.which("gcc") or shutil.which("cc") + cls.fresh = cls.configure("fresh") + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + @classmethod + def configure(cls, name, *extra): + return subprocess.run(["cmake", *extra, "-S", str(cls.root / "source"), "-B", str(cls.root / name), "-G", "Ninja", + f"-DCMAKE_C_COMPILER={cls.compiler}"], check=True, capture_output=True, text=True).stdout + + def seed(self, cache, name): + seed = self.root / name + subprocess.run([sys.executable, str(TOOL), str(cache), str(seed)], check=True, capture_output=True) + return seed + + def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): + self.assertRegex(self.fresh, r"Looking for|Performing Test") + output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) + self.assertNotRegex(output, r"Looking for|Performing Test") + fresh = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual(configure_seed.checks(self.root / "seeded" / "CMakeCache.txt"), fresh) + self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") + self.assertEqual(fresh["MINI_FAILS"][0], "") + + def test_only_check_results_are_seeded(self): + found = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual({n for n in found if n.startswith("MINI_")}, + {"MINI_HAVE_STDIO_H", "MINI_HAVE_MISSING_H", "MINI_HAVE_PRINTF", "MINI_COMPILES", "MINI_FAILS"}) + self.assertNotIn("CMAKE_C_COMPILER", found) + + def test_values_with_cmake_syntax_round_trip(self): + cache = self.root / "odd" / "CMakeCache.txt" + cache.parent.mkdir() + value = r'a;b "c" \d ${e} $f' + cache.write_text(f"//Test MINI_ODD\nMINI_ODD:INTERNAL={value}\n", encoding="utf-8") + self.configure("odd-seeded", "-C", str(self.seed(cache, "odd.cmake"))) + seeded = (self.root / "odd-seeded" / "CMakeCache.txt").read_text(encoding="utf-8") + self.assertIn(f"MINI_ODD:INTERNAL={value}\n", seeded) + self.assertEqual(configure_seed.checks(self.root / "odd-seeded" / "CMakeCache.txt")["MINI_ODD"][0], value) + + def test_cache_without_checks_writes_an_empty_seed(self): + cache = self.root / "empty" / "CMakeCache.txt" + cache.parent.mkdir() + cache.write_text("//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n", encoding="utf-8") + self.assertEqual(self.seed(cache, "empty.cmake").read_text(encoding="utf-8"), "") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 7e54432bf2..14eb2bac77 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -25,6 +25,17 @@ ORIGINAL = index([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED]) class DebtSplitTests(unittest.TestCase): + def test_split_preserves_root_relative_links(self): + targets = ("/AnyPS5/guide.html#usage", "/images/example.png", "/") + references = " ".join(f"[reference]({target})" for target in targets) + self.write(INDEX, ORIGINAL.replace(A_STUB, A_STUB + " " + references)) + self.split() + moved = self.read("docs/dev/debt/libSceA.md") + for target in targets: + with self.subTest(target=target): + self.assertIn(f"[reference]({target})", moved) + self.assertIn("../../../core/libs/prx/libSceA/Export.cpp", moved) + def setUp(self): self.directory = tempfile.TemporaryDirectory() self.root = Path(self.directory.name) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 9a0359e654..b8557abef7 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -136,6 +136,39 @@ def build_pe(names): return bytes(image) + raw + b"\xc3" * (16 * max(1, len(names))) +def build_pe_with_imports(dlls, names=()): + idata_rva, edata_rva, text_rva, raw_offset = 0x1000, 0x2000, 0x3000, 0x200 + directory, strings = b"", b"" + for dll in dlls: + directory += struct.pack(" libSceIme.prx", out) + self.assertIn("Importer: libSceVideoOut.prx Chain: libSceVideoOut.prx -> libSceKeyboard.prx", out) + self.assertNotIn("KERNEL32", out) + + def test_transitive_host_dependency_reports_chain(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceB.prx"], ["AAAAAAAAAAA"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["libSceMissing.prx"], ["BBBBBBBBBBB"])) + registry = work.root / "game.json" + write_registry(registry, []) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "libSceMissing.prx"]}, + {"importer": "libSceB.prx", "chain": ["libSceB.prx", "libSceMissing.prx"]}, + ]) + self.assertIn("Importer: libSceA.prx Chain: libSceA.prx -> libSceB.prx -> libSceMissing.prx", out) + + def test_host_dependency_satisfied_by_module(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceIme.prx"], ["AAAAAAAAAAA"])) + (work.modules / "libSceIme.prx").write_bytes(exporting()) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, "")) + self.assertNotIn("Host dependencies", out) + + def test_complete_host_dependencies_pass(self): + work = self.work + (work.libs / "libc.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) + (work.libs / "libSceVideoOut.prx").write_bytes(build_pe_with_imports(["libc.prx"], ["BBBBBBBBBBB"])) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, "")) + self.assertNotIn("Host dependencies", out) + + def test_elf_host_dependencies_are_walked(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_elf( + [("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], ["libSceGhost.prx", "KERNEL32.dll"])) + registry = work.root / "game.json" + write_registry(registry, []) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceGhost.prx"]}, + ]) + self.assertNotIn("KERNEL32", out) + + def test_conflicting_host_libraries_are_ambiguous(self): + first, second = self.work.libs, self.work.root / "other-libs" + second.mkdir() + (first / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceB.prx"], ["AAAAAAAAAAA"])) + (second / "libSceA.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) + with self.assertRaisesRegex(import_audit.AuditError, "ambiguous"): + import_audit.built_host_dependencies([first, second]) + def test_elf_unnamed_defined_symbol_is_not_an_export(self): image = bytearray(exporting("alpha")) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 213f79c47f..ff04a1edab 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -1,6 +1,8 @@ import contextlib import io import json +import os +import subprocess import tempfile import sys import unittest @@ -13,6 +15,21 @@ import nid_names class DatabaseCacheTests(unittest.TestCase): + def test_empty_symbol_filters_do_not_open_database(self): + with tempfile.TemporaryDirectory() as directory: + for option in ("--name", "--symbol", "--na", "--sym"): + for json_output in (False, True): + with self.subTest(option=option, json_output=json_output): + result = subprocess.run( + [sys.executable, str(Path(nid_names.__file__)), option, "--db", directory, + *(["--json"] if json_output else [])], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, "") + if json_output: + self.assertEqual(json.loads(result.stdout), {"symbols": []}) + else: + self.assertEqual(result.stdout.split(), ["NAME", "NID"]) + def test_interrupted_download_does_not_publish_a_partial_cache(self): with tempfile.TemporaryDirectory() as directory: cache = Path(directory) / "cache" / "aerolib.csv" @@ -203,6 +220,31 @@ class SourceCollectionTests(unittest.TestCase): path.write_text(text, encoding="utf-8") return path + def cli(self, *arguments): + tools = self.root / "tools" + tools.mkdir(exist_ok=True) + tool = tools / "nid_names.py" + tool.write_bytes(Path(nid_names.__file__).read_bytes()) + return subprocess.run([sys.executable, str(tool), *map(str, arguments)], + env={**os.environ, "PYTHONIOENCODING": "cp1252"}, + capture_output=True, timeout=10) + + def test_report_preserves_unicode_paths_with_legacy_stdout(self): + self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n', "\u8cc7\u6599.cpp") + database = self.root / "aerolib.csv" + database.write_bytes(b"") + result = self.cli("--db", database) + self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) + self.assertIn("core/libs/prx/libSceExample/\u8cc7\u6599.cpp", result.stdout.decode("utf-8")) + + def test_error_preserves_unicode_paths_with_legacy_stderr(self): + database = self.root / "\u8cc7\u6599" + database.mkdir() + result = self.cli("--db", database) + self.assertEqual(result.returncode, 2, result.stderr.decode("utf-8", errors="replace")) + self.assertIn("\u8cc7\u6599", result.stderr.decode("utf-8")) + self.assertNotIn(b"Traceback", result.stderr) + def test_comments_and_literals_do_not_create_export_records(self): self.write_source('''// APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00); /* int APS5_VABI sceCommented() { return 0; } */ @@ -214,6 +256,20 @@ int APS5_VABI sceReal() { return 0; } self.assertEqual(nid_names.collect_unknowns(), {"CCCCCCCCCCC": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown02"}) self.assertEqual(nid_names.collect_real_names(), {"sceReal"}) + def test_assembly_export_macros_are_real_names(self): + self.write_source('''asm(".text\n" + APS5_ASM_FUNCTION("sceAsmCall_nid_postfix") + "ret\n" + APS5_SIGNAL_ASM_FUNCTION("sigAsmCall_nid_postfix") + "ret\n"); +// APS5_ASM_FUNCTION("sceCommented_nid_postfix") +auto example = "APS5_ASM_FUNCTION(\"sceQuoted_nid_postfix\")"; +''') + self.assertEqual(nid_names.collect_real_names(), { + "sceAsmCall", "sceAsmCall_nid_postfix", + "sigAsmCall", "sigAsmCall_nid_postfix", + }) + def test_formatted_alias_macro_is_detected(self): self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 0de3c558e8..0f779c1326 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -2,6 +2,7 @@ import os import json import ctypes import stat +import subprocess import sys import tarfile import tempfile @@ -232,6 +233,33 @@ class ReleasePackageTests(unittest.TestCase): finally: destination.chmod(stat.S_IREAD | stat.S_IWRITE) + @unittest.skipUnless(os.name == "nt", "Windows directory junction replacement semantics") + def test_dangling_directory_junction_preserves_all_existing_assets(self): + names = ["prx-linux-v1.zip", "prx-linux-v1.tar.gz", "relinker-v1"] + for index, blocked in enumerate(names): + with self.subTest(blocked=blocked): + output = self.output / str(index) + output.mkdir(parents=True) + previous = {name: b"old " + name.encode() for name in names if name != blocked} + previous["unrelated.txt"] = b"unrelated" + for name, content in previous.items(): + (output / name).write_bytes(content) + target = self.root / f"target-{index}" + target.mkdir() + destination = output / blocked + subprocess.run(["cmd", "/c", "mklink", "/J", str(destination), str(target)], + check=True, capture_output=True) + self.addCleanup(destination.rmdir) + target.rmdir() + self.assertFalse(destination.exists()) + self.assertTrue(destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY) + with self.assertRaises((OSError, RuntimeError)) as error: + package_release.package("linux", self.build, output, "v1") + self.assertEqual({path.name: path.read_bytes() for path in output.iterdir() if path != destination}, previous) + self.assertTrue(destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY) + self.assertFalse(target.exists()) + self.assertIn("not a file", str(error.exception)) + def file_link(self, link, target): try: link.symlink_to(target) @@ -384,6 +412,31 @@ class ReleaseAssetsTests(unittest.TestCase): class ReleaseNotesTests(unittest.TestCase): + def test_aliased_paths_preserve_input_and_existing_outputs(self): + for pair in ((0, 1), (0, 2), (1, 2)): + for alias in ("same", "symlink", "hardlink"): + with self.subTest(pair=pair, alias=alias), tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / name for name in ("notes.json", "body.md", "full.md")] + paths[0].write_text(json.dumps({"body": "release notes"}), encoding="utf-8") + paths[1].write_bytes(b"previous body") + paths[2].write_bytes(b"previous full") + first, second = pair + paths[second].unlink() + if alias == "same": + paths[second] = paths[first] + elif alias == "symlink": + try: + paths[second].symlink_to(paths[first]) + except OSError as error: + self.skipTest(str(error)) + else: + os.link(paths[first], paths[second]) + previous = [path.read_bytes() for path in paths] + with self.assertRaisesRegex(ValueError, "distinct files"): + prepare_release_notes.prepare_notes(*paths) + self.assertEqual([path.read_bytes() for path in paths], previous) + + def test_short_notes_are_unchanged_including_unicode_at_limit(self): for notes in ("", "## What's Changed\n* Fix\n", "я" * 125000): self.assertEqual(prepare_release_notes.release_body(notes), notes) diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 38c9514571..01b48a9e68 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -77,6 +77,10 @@ class PadInputTests(unittest.TestCase): self.assertEqual(self.queue(2000, "release", "--right-stick", "0", "255"), [2000, 0, 300, 128, 128, 0, 255]) + def test_touchpad_halves_queue_their_bits_and_preserve_sequence(self): + self.assertEqual(self.queue(1000, "touch-left"), [1000, 0x200000, 300]) + self.assertEqual(self.queue(1000, "touch-right"), [1001, 0x400000, 300]) + def test_valid_stick_command_and_unsigned_tokens_preserve_sequence(self): self.path.write_text("+2000 +16384 +300 +0 +255 +128 +128\n", encoding="utf-8") self.assertEqual(self.queue(1000, "release"), [2001, 0, 300]) diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 7cba7c6deb..4b27dd7159 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -193,6 +193,14 @@ class PerformanceCompareTests(unittest.TestCase): self.assertEqual(result.returncode, 2) self.assertIn("avant-\u00e9.jsonl:1:", result.stderr) + def test_bom_summary_loads(self): + self.before.write_text(json.dumps(self.baseline) + "\n", encoding="utf-8-sig") + self.write(self.after, self.candidate) + self.assertEqual(performance_compare.load_summary(self.before), self.baseline) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)["baseline"], self.baseline) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index e76ab38727..c8245f7112 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -1,5 +1,6 @@ import json import os +import re import shutil import subprocess import sys @@ -7,6 +8,7 @@ import tempfile import unittest from pathlib import Path from unittest.mock import patch +from urllib.parse import unquote, urlsplit sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import pr_overlap @@ -216,6 +218,14 @@ class ConflictTests(unittest.TestCase): self.assertIn("`shared.cpp`", report) self.assertTrue(report.startswith(pr_overlap.MARKER)) + def test_report_links_pull_requests_without_cross_references(self): + rows = {2: {"added": set(), "conflicts": {"other.cpp": [(1, 1)]}}, + 3: {"added": set(), "uncompared": {"shared.cpp"}}} + report = pr_overlap.report(rows, "https://github.com/owner/project/blob/base") + self.assertIsNone(re.search(r"#\d|https://github\.com/owner/project/(pull|issues)/", report)) + self.assertIn("[PR 2](https://www.github.com/owner/project/pull/2)", report) + self.assertIn("[PR 3](https://www.github.com/owner/project/pull/3)", report) + class UnicodePathTests(unittest.TestCase): def test_scans_and_reports_conflicts_with_git_path_quoting_enabled(self): @@ -229,7 +239,7 @@ class UnicodePathTests(unittest.TestCase): "-c", "commit.gpgsign=false", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() - filename = "core/libs/prx/libSceExample/caf\u00e9.cpp" + filename = "core/libs/prx/libSceExample/caf\u00e9 #1 (%20).cpp" source = root / filename source.parent.mkdir(parents=True) source.write_text("int value = 0;\n", encoding="utf-8") @@ -252,6 +262,13 @@ class UnicodePathTests(unittest.TestCase): self.assertEqual(left["files"], {filename}) self.assertEqual(right["files"], {filename}) self.assertEqual(pr_overlap.conflicts(left, right), {filename: [(1, 1)]}) + blob = f"https://github.com/owner/project/blob/{base}" + message = pr_overlap.report({7: {"conflicts": pr_overlap.conflicts(left, right), "added": set()}}, blob) + link = message.split("[L1](", 1)[1].split(")", 1)[0] + parsed = urlsplit(link) + self.assertEqual(unquote(parsed.path), f"/owner/project/blob/{base}/{filename}") + self.assertEqual(parsed.fragment, "L1") + self.assertEqual(parsed.query, "") finally: os.chdir(previous) @@ -283,8 +300,8 @@ class CommentStateTests(unittest.TestCase): return pr_overlap.report(found, f"https://github.com/owner/project/blob/{commit}") def run_step(self, report, comment): - (self.root / "overlap" / "42.md").write_text(report, encoding="utf-8", newline="\n") - (self.root / "comment").write_text(json.dumps([5, comment]) + "\n" if comment else "", encoding="utf-8", newline="\n") + (self.root / "overlap" / "42.md").write_bytes(report.encode("utf-8")) + (self.root / "comment").write_bytes((json.dumps([5, comment]) + "\n" if comment else "").encode("utf-8")) calls = self.root / "calls" calls.write_text("", encoding="utf-8") script = "\n".join([ @@ -293,7 +310,7 @@ class CommentStateTests(unittest.TestCase): 'gh() { printf \'%s\\n\' "$*" >> "$RUNNER_TEMP/calls"; case "$*" in "api repos/"*) cat "$RUNNER_TEMP/comment";; esac; }', self.step, ]) - (self.root / "step.sh").write_text(script, encoding="utf-8", newline="\n") + (self.root / "step.sh").write_bytes(script.encode("utf-8")) environment = dict(os.environ, RUNNER_TEMP=str(self.root), GH_REPO="owner/project") result = subprocess.run(["bash", "step.sh"], cwd=self.root, env=environment, capture_output=True, text=True, timeout=20) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) diff --git a/tools/tests/test_pr_overlap_own_branch.py b/tools/tests/test_pr_overlap_own_branch.py index c27c8624e9..802d57c74b 100644 --- a/tools/tests/test_pr_overlap_own_branch.py +++ b/tools/tests/test_pr_overlap_own_branch.py @@ -3,6 +3,7 @@ import subprocess import sys import tempfile import unittest +from unittest.mock import patch from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -33,6 +34,18 @@ class OwnBranchTests(unittest.TestCase): finally: os.chdir(old) + def test_scans_all_pull_requests_with_a_bounded_worker_pool(self): + prs = {number: {"sha": str(number)} for number in range(1, 9)} + scanned = [] + + def scan(pr): + scanned.append(pr["sha"]) + + with patch.object(pr_overlap, "scan", side_effect=scan): + pr_overlap.scan_all(prs, 3) + + self.assertCountEqual(scanned, [str(number) for number in prs]) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress_render_sliver.py b/tools/tests/test_progress_render_sliver.py index 4fa8b21948..cf08a54344 100644 --- a/tools/tests/test_progress_render_sliver.py +++ b/tools/tests/test_progress_render_sliver.py @@ -13,7 +13,10 @@ class ProgressRenderSliverTests(unittest.TestCase): {"name": "libA", "label": "libA", "done": 300, "todo": 0}, {"name": "libB", "label": "libB", "done": 1, "todo": 0}]} shaders = {"done": 0, "total": 0, "percent": 0, "groups": []} - self.assertIn("libA", svg) + self.assertNotIn(">lib", svg) def test_cells_without_area(self): self.assertEqual(progress.cells(5, 0, 0, 0, 10), []) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py new file mode 100644 index 0000000000..192a533da4 --- /dev/null +++ b/tools/tests/test_select_tests.py @@ -0,0 +1,150 @@ +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +TOOL = Path(__file__).resolve().parents[1] / "select_tests.py" +sys.path.insert(0, str(TOOL.parent)) + +import select_tests + +PROJECT = { + "CMakeLists.txt": "cmake_minimum_required(VERSION 3.20)\nproject(mini C)\nenable_testing()\n" + "add_subdirectory(alpha)\nadd_subdirectory(beta)\n" + "add_test(NAME script COMMAND ${CMAKE_COMMAND} -E true)\n", + "alpha/CMakeLists.txt": "add_executable(alpha_tests main.c)\nadd_test(NAME alpha COMMAND alpha_tests)\n" + "add_test(NAME alpha_data COMMAND alpha_tests ${CMAKE_SOURCE_DIR}/data/input.txt)\n", + "alpha/main.c": "#include \"alpha.h\"\nint main(void) { return ALPHA; }\n", + "alpha/alpha.h": "#define ALPHA 0\n", + "beta/CMakeLists.txt": "add_executable(beta_tests main.c)\nadd_executable(setup_tests setup.c)\n" + "add_test(NAME beta COMMAND beta_tests)\nadd_test(NAME beta_setup COMMAND setup_tests)\n" + "set_tests_properties(beta_setup PROPERTIES FIXTURES_SETUP beta_fixture)\n" + "set_tests_properties(beta PROPERTIES FIXTURES_REQUIRED beta_fixture)\n", + "beta/main.c": "int main(void) { return 0; }\n", + "beta/setup.c": "int main(void) { return 0; }\n", + "data/input.txt": "input\n", +} + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class SelectTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + (Path(cls.directory.name) / "real").mkdir() + root = Path(cls.directory.name) / "link" + try: + root.symlink_to("real", target_is_directory=True) + except OSError as error: + if getattr(error, "winerror", None) != 1314: + raise + import _winapi + _winapi.CreateJunction(str(Path(cls.directory.name) / "real"), str(root)) + cls.source, cls.build, cls.graph = root / "source", root / "build", root / "graph.json" + for name, content in PROJECT.items(): + (cls.source / name).parent.mkdir(parents=True, exist_ok=True) + (cls.source / name).write_text(content, encoding="utf-8") + compiler = shutil.which("gcc") or shutil.which("cc") + subprocess.run(["cmake", "-S", str(cls.source), "-B", str(cls.build), "-G", "Ninja", f"-DCMAKE_C_COMPILER={compiler}"], + check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(cls.build)], check=True, capture_output=True) + cls.tool("dump", cls.build, cls.graph) + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + @staticmethod + def tool(*args): + return subprocess.run([sys.executable, str(TOOL), *map(str, args)], check=True, capture_output=True, text=True).stdout + + def select(self, *changed, graph=None): + listed = self.build / "changed.txt" + listed.write_text("".join(f"{c}\n" for c in changed), encoding="utf-8") + output = self.tool("select", self.build, graph or self.graph, listed) + tests = self.build / "select.tests" + if not tests.exists(): + self.assertFalse((self.build / "select.ninja").exists()) + return output, None + shown = subprocess.run(["ctest", "--test-dir", str(self.build), "-N", "-I", tests.read_text(encoding="utf-8").strip()], + check=True, capture_output=True, text=True).stdout + return output, set(re.findall(r"Test +#\d+: (\S+)", shown)) + + def test_header_selects_the_tests_of_its_executable(self): + self.assertEqual(self.select("alpha/alpha.h")[1], {"alpha", "alpha_data", "script"}) + + def test_argument_file_selects_only_the_test_that_reads_it(self): + self.assertEqual(self.select("data/input.txt")[1], {"alpha_data", "script"}) + + def test_fixture_setup_runs_with_its_test(self): + self.assertEqual(self.select("beta/main.c")[1], {"beta", "beta_setup", "script"}) + self.assertEqual(self.select("beta/setup.c")[1], {"beta_setup", "script"}) + + def test_cmake_file_selects_the_tests_it_registers(self): + self.assertEqual(self.select("beta/CMakeLists.txt")[1], {"beta", "beta_setup", "script"}) + + def test_file_outside_every_build_selects_its_directory(self): + self.assertEqual(self.select("alpha/notes.h")[1], {"alpha", "alpha_data", "script"}) + self.assertEqual(self.select("README.md")[1], {"script"}) + + def test_global_files_run_everything(self): + for changed in ("CMakeLists.txt", "cmake/Toolchain.cmake", ".github/workflows/build.yml", "3rdparty/x/y.c", + "tools/select_tests.py"): + with self.subTest(changed=changed): + output, selected = self.select("alpha/alpha.h", changed) + self.assertIsNone(selected) + self.assertIn(changed, output) + + def test_missing_graph_runs_everything(self): + self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) + + def test_test_missing_from_the_graph_runs(self): + graph = json.loads(self.graph.read_text(encoding="utf-8")) + del graph["tests"]["beta"] + older = self.build / "older.json" + older.write_text(json.dumps(graph), encoding="utf-8") + self.assertEqual(self.select("alpha/alpha.h", graph=older)[1], {"alpha", "alpha_data", "beta", "beta_setup", "script"}) + + def test_selected_target_skips_unselected_executables(self): + self.select("alpha/main.c") + subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + built = {p.stem for p in self.build.rglob("*_tests*") if p.is_file() and os.access(p, os.X_OK)} + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + self.assertEqual(built, {"alpha_tests"}) + + +class CaseInsensitivePathTests(unittest.TestCase): + def build(self): + build = object.__new__(select_tests.Build) + build.path, build.source = os.path.join(os.sep, "Work", "Source", "build"), os.path.join(os.sep, "Work", "Source") + return build + + def test_paths_keep_their_case_when_comparisons_ignore_it(self): + with mock.patch.object(select_tests.os.path, "normcase", str.lower): + build = self.build() + self.assertEqual(build.output(os.path.join(os.sep, "work", "source", "build", "tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.output(os.path.join("tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.repo(os.path.join(os.sep, "work", "source", "core", "libSceAgc", "Driver.cpp")), "core/libSceAgc/Driver.cpp") + self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) + self.assertEqual(build.key(os.path.join("tests", "MemoryPoolTest.exe")), build.key(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "TESTS", "memorypooltest.exe"))) + + def test_trailing_separator_in_root_is_handled(self): + with mock.patch.object(select_tests.os.path, "normcase", str.lower): + build = self.build() + build.source = build.source + os.sep + build.path = build.path + os.sep + self.assertEqual(build.output(os.path.join(os.sep, "work", "source", "build", "tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.output(os.path.join("tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.repo(os.path.join(os.sep, "work", "source", "core", "libSceAgc", "Driver.cpp")), "core/libSceAgc/Driver.cpp") + self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index a5de76dba1..b53754fcb7 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -12,6 +12,20 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + def test_disabled_property_is_local_to_each_configuration(self): + for configurations in (("OFF", "TRUE"), ("TRUE", "OFF"), ("TRUE", "TRUE")): + with self.subTest(configurations=configurations): + content = 'if(CTEST_CONFIGURATION_TYPE STREQUAL "Debug")\n' + for index, disabled in enumerate(configurations): + if index: + content += 'else()\n' + content += f'add_test("bounds" "/build/tests/config{index}/buffer_bounds_tests")\n' + content += f'set_tests_properties("bounds" PROPERTIES DISABLED {disabled})\n' + content += 'endif()\n' + names = self.registered(content) + self.assertEqual("buffer_bounds_tests" in names, "OFF" in configurations) + + def registered(self, content): with tempfile.TemporaryDirectory() as directory: build = Path(directory) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index d9dd888b29..c46ba26290 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -46,6 +46,25 @@ STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll class TraceDigestTests(unittest.TestCase): + def test_cli_reports_unreadable_inputs_without_partial_output(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + compressed = gzip.compress(b'{"traceEvents": []}') + files = {"invalid.json": b"{", "broken.json.gz": b"not gzip", "intervals.jsonl": b"{\n", + "truncated.json.gz": compressed[:-4], + "corrupt.json.gz": compressed[:10] + b"\xff" * 4 + compressed[14:]} + for name, data in files.items(): + (root / name).write_bytes(data) + for name in ("missing.json", *files): + with self.subTest(name=name): + result = subprocess.run([sys.executable, trace_digest.__file__, str(root / name)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertTrue(result.stderr.startswith("FAIL: "), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + + def setUp(self): self.directory = tempfile.TemporaryDirectory() root = Path(self.directory.name) @@ -97,6 +116,14 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(thread["folded_ms_per_frame"], 1.5) self.assertEqual(thread["untraced_ms_per_frame"], 0) + def test_trace_accepts_event_array_and_empty_document(self): + trace_from_array = trace_digest.Trace(EVENTS) + self.assertEqual(len(trace_from_array.presents), 3) + empty_dict = trace_digest.Trace({}) + self.assertEqual(empty_dict.presents, []) + empty_list = trace_digest.Trace([]) + self.assertEqual(empty_list.presents, []) + 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) @@ -176,6 +203,23 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(largest[("Driver.Present", "presenter")], 16.0) self.assertAlmostEqual(largest[("Driver.Present.inflight_wait", "presenter")], 14.5) + def test_markdown_thread_names_cannot_split_table_cells_or_rows(self): + name = "worker|\nrow & more" + phase = "phase|\r\nnext" + trace = trace_digest.Trace({"traceEvents": [ + {"ph": "M", "name": "thread_name", "tid": 1, "args": {"name": name}}, + complete(1, phase, 0, 1000), + ]}) + result = trace_digest.digest(trace) + self.assertEqual(result["threads"][0]["thread"], name) + self.assertEqual(result["threads"][0]["top"][0]["name"], phase) + rows = [line for line in trace_digest.markdown(result).splitlines() if line.startswith("|")] + self.assertEqual(len(rows), 3) + self.assertTrue(all(line.count("|") == 7 for line in rows), rows) + self.assertIn("worker|<gpu>
row & more", rows[2]) + self.assertIn("phase|<tag>
next", rows[2]) + + def test_threads_split_busy_and_wait(self): threads = {thread["thread"]: thread for thread in self.result()["threads"]} worker = threads["queue worker 0x0"] @@ -234,6 +278,20 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(thread["wait_ms_per_frame"], 4.0) self.assertEqual(thread["top"], [{"name": "GpuMutex.hold.draw", "ms_per_frame": 6.0}]) + def test_invalid_stack_weights_fail_without_a_digest(self): + for row in ("worker;draw -1", "worker;draw 0", "worker;draw nope", "worker;;draw 1", " 1"): + with self.subTest(row=row): + self.stacks.write_text("# header\n" + row + "\n", encoding="utf-8") + output, errors = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors): + with self.assertRaises(SystemExit) as exit: + trace_digest.main([str(self.trace), "--stacks", str(self.stacks), "--json"]) + self.assertEqual(exit.exception.code, 2) + self.assertEqual(output.getvalue(), "") + self.assertIn(str(self.stacks) + ":2:", errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) + + def test_stacks_self_and_total(self): stacks = self.result()["stacks"] self.assertEqual(stacks["samples"], 5) @@ -272,6 +330,26 @@ class TraceDigestTests(unittest.TestCase): def test_no_batch_section_without_batch_ids(self): self.assertNotIn("largest_gpu_batches", self.result()) + def test_negative_list_limits_are_rejected_and_zero_is_supported(self): + for option in ("--frames", "--top"): + with self.subTest(option=option): + output, errors = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors): + with self.assertRaises(SystemExit) as exit: + trace_digest.main([str(self.trace), option, "-1", "--json"]) + self.assertEqual(exit.exception.code, 2) + self.assertEqual(output.getvalue(), "") + self.assertIn(option, errors.getvalue()) + self.assertIn("nonnegative", errors.getvalue()) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--frames", "0", "--top", "0", "--json"]), 0) + result = json.loads(output.getvalue()) + self.assertEqual(result["slowest_frames"], []) + self.assertEqual(result["top_slices_ms_per_frame"], []) + self.assertEqual(result["longest_waits"], []) + + def test_command_line_markdown_and_json(self): output = io.StringIO() with contextlib.redirect_stdout(output): @@ -377,5 +455,29 @@ class IntervalDigestTests(unittest.TestCase): trace_digest.main([str(self.path)]) self.assertIn("no interval lines", output.getvalue()) + def test_interval_phase_names_preserve_json_and_markdown_table_structure(self): + for name, escaped in (("Draw|&", "Draw|<pass>&"), + ("Draw\r\npass", "Draw
pass"), + ("Draw\rpass\nend", "Draw
pass
end")): + with self.subTest(name=name): + line = interval_line(0.0, 30.0, 20.0, 5.0, top_phases_ms_per_frame={name: 7.5}) + self.path.write_text(json.dumps(line) + "\n", encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path), "--json"]), 0) + row = json.loads(output.getvalue())["intervals"][0] + self.assertEqual(row["top_phase"], {"name": name, "ms_per_frame": 7.5}) + self.assertEqual(row["frame_ms"], line["frame_ms"]) + self.assertEqual(row["gpu_busy_ms_per_present"], 5.0) + self.assertEqual((row["start_s"], row["end_s"], row["presents"]), (0.0, 30.0, 1800)) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path)]), 0) + rows = [line for line in output.getvalue().splitlines() if line.startswith("|")] + self.assertEqual(len(rows), 3) + self.assertTrue(all(line.count("|") == 11 for line in rows), rows) + cells = [cell.strip() for cell in rows[2].split("|")[1:-1]] + self.assertEqual(cells, ["1", "0-30", "1800", "16.6", "18.0", "20.0", "25.0", "5.0", "50 %", escaped + " 7.5"]) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 44e1897ccb..05436dd626 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -13,6 +13,7 @@ ELF_HEADER_OFFSET = 0x1A0 PROGRAM_TABLE_OFFSET = 0x40 PT_LOAD = 1 PT_DYNAMIC = 2 +PT_TLS = 7 FIRST_REGION = 0x400 HEADER = b"\x7fELF\x02\x01\x01\x09" + b"\x00" * 56 @@ -23,18 +24,19 @@ def pattern(seed, size): return bytes((seed + index * 7) % 251 for index in range(size)) -def build_elf(segments): +def build_elf(segments, program_offset=PROGRAM_TABLE_OFFSET): header = bytearray(HEADER) struct.pack_into("") + + def markdown(result): lines = [] summary = result["summary"] @@ -217,8 +230,8 @@ def markdown(result): 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(f"| {table_cell(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} | {table_cell(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("") @@ -289,28 +302,39 @@ def markdown_intervals(result): worker = f"{row['busiest_worker']['pct']:.0f} %" if row["busiest_worker"] else "-" phase = f"{row['top_phase']['name']} {row['top_phase']['ms_per_frame']:.1f}" if row["top_phase"] else "-" lines.append(f"| {index} | {row['start_s']:.0f}-{row['end_s']:.0f}{' (dropped)' if row['trace_dropped'] else ''} | {row['presents']} | {frame['p50']:.1f} | {frame['p90']:.1f} " - f"| {frame['p99']:.1f} | {frame['max']:.1f} | {row['gpu_busy_ms_per_present']:.1f} | {worker} | {phase} |") + f"| {frame['p99']:.1f} | {frame['max']:.1f} | {row['gpu_busy_ms_per_present']:.1f} | {worker} | {table_cell(phase)} |") p99 = [row["frame_ms"]["p99"] for row in rows] gpu = [row["gpu_busy_ms_per_present"] for row in rows] lines += ["", "```", f"p99 ms {sparkline(p99)} 0-{max(p99):.1f}", f"gpu ms {sparkline(gpu)} 0-{max(gpu):.1f}", "```"] return "\n".join(lines) + "\n" +def nonnegative_integer(value): + number = int(value) + if number < 0: + raise argparse.ArgumentTypeError("must be a nonnegative integer") + return number + + def main(arguments=None): parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (.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("--frames", type=nonnegative_integer, default=5, help="slowest frames to list (default 5)") + parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) if options.trace.endswith(".jsonl"): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) return 0 + try: + stacks = load_stacks(options.stacks) if options.stacks else None + except (OSError, ValueError) as error: + parser.error(str(error)) result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), - load_stacks(options.stacks) if options.stacks else None, options.frames, options.top) + stacks, options.frames, options.top) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) return 0 @@ -318,4 +342,8 @@ def main(arguments=None): if __name__ == "__main__": if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.exit(main()) + try: + sys.exit(main()) + except (EOFError, OSError, ValueError, zlib.error) as error: + print(f"FAIL: {error}", file=sys.stderr) + sys.exit(2) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 05af4b508a..5e16158026 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -94,17 +94,28 @@ def parse_elf(data): inner = data.find(b"\x7fELF") if inner < 0: raise Refused("no inner ELF header") + if inner + 0x40 > len(data): + raise Refused("inner ELF header is truncated") header = bytearray(data[inner:inner + 0x40]) program_offset = struct.unpack_from(" len(data): + raise Refused("ELF program header table does not fit the file") headers = [] for index in range(count): offset = inner + program_offset + index * 56 kind, flags, file_offset, vaddr, _, file_size, memory_size, alignment = struct.unpack_from( " memory_size: + raise Refused("segment %d file size exceeds memory size" % index) + if alignment and alignment & (alignment - 1): + raise Refused("segment %d alignment is not a power of two" % index) + if memory_size > (1 << 64) - vaddr: + raise Refused("segment %d memory range exceeds ELF64" % index) headers.append({ "index": index, "type": kind, "flags": flags, "offset": file_offset, "vaddr": vaddr, "size": file_size, "memory": memory_size, @@ -127,10 +138,11 @@ def place_segments(data, headers, records): header["source"] = segment break - def load_for(vaddr): + def load_for(vaddr, size): found = None for header in loads: - if header["memory"] and header["vaddr"] <= vaddr < header["vaddr"] + header["memory"]: + end = header["vaddr"] + header["memory"] + if header["memory"] and header["vaddr"] <= vaddr and (vaddr < end or (size == 0 and vaddr == end)): found = header return found @@ -139,8 +151,11 @@ def place_segments(data, headers, records): if header["type"] == PT_LOAD: continue if header["vaddr"] != 0: - host = load_for(header["vaddr"]) + host = load_for(header["vaddr"], header["size"]) if host is None: + if header["size"] == 0: + header["source"] = 0 + continue raise Refused("segment %d vaddr %#x is in no PT_LOAD" % (header["index"], header["vaddr"])) header["host"] = host header["source"] = host["source"] + (header["vaddr"] - host["vaddr"]) @@ -155,6 +170,8 @@ def place_segments(data, headers, records): header["memory"] = min(header["memory"], keep) for header in headers: + if header["type"] == PT_LOAD and header["size"] == 0: + continue if header["source"] > len(data) or header["size"] > len(data) - header["source"]: raise Refused("segment %d does not fit the file" % header["index"]) @@ -174,7 +191,10 @@ def check_decrypted(data, headers): def unwrap(path, output, verbose=False): - data = bytes(Path(path).read_bytes()) + source, destination = Path(path), Path(output) + if source.resolve() == destination.resolve() or (destination.exists() and source.samefile(destination)): + raise Refused("output aliases the input container") + data = bytes(source.read_bytes()) if len(data) < FIRST_REGION: raise Refused("file is smaller than the SELF header area") magic = struct.unpack_from("