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/build.yml b/.github/workflows/build.yml index 328669a882..cab59565e5 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,23 +70,77 @@ 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([ ...(full && linux ? [] : [{ os: 'ubuntu-latest', compiler: 'g++' }]), ...(full ? [] : [{ os: 'windows-latest', compiler: 'g++' }]), { os: 'macos-latest', compiler: 'clang++' }, + { os: 'macos-latest', compiler: 'clang++', architectures: 'x86_64' }, ]); if (context.eventName === 'push') { if (context.ref !== 'refs/heads/main') { @@ -97,6 +156,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 +222,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); @@ -254,9 +315,40 @@ jobs: 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null - if: runner.os == 'Windows' 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 + - if: ${{ matrix.architectures == 'x86_64' }} + run: sudo softwareupdate --install-rosetta --agree-to-license + - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }} - 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 +362,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 +396,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 +430,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 +463,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 +571,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 +587,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 +628,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 +703,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/pr-intake.yml b/.github/workflows/pr-intake.yml index 949a8cb717..3225af812b 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -27,6 +27,27 @@ jobs: if (pr.state !== 'open' || pr.base.ref !== 'main') { return; } + if (context.payload.action === 'reopened') { + let maintainer = false; + try { + const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, repo, username: context.payload.sender.login + }); + maintainer = ['admin', 'maintain', 'write'].includes(access.role_name ?? access.permission); + } catch (error) { + core.warning(`Could not check the permission of ${context.payload.sender.login}, so the open PR limit applies: ${error.message}`); + } + if (maintainer) { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name: 'open PR limit' }); + } catch (error) { + if (error.status !== 404) { + core.warning(`Could not remove the open PR limit label: ${error.message}`); + } + } + return; + } + } const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({ q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`, per_page: 1 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..48ecc49c93 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) @@ -86,6 +97,17 @@ if(BUILD_TESTING) add_test(NAME request_serializer COMMAND request_serializer_tests) set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) + add_test_executable(shader_ir_use_rewrite_tests + core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp + core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp + core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp + core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp + core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp + core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp) + target_include_directories(shader_ir_use_rewrite_tests PRIVATE + core/shader/recompiler/IntermediateRepresentation/include) + add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests) + set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -113,14 +135,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py) - foreach(shard RANGE 0 5) + foreach(shard RANGE 0 11) if(shard EQUAL 0) set(conventions_test_name conventions_function_bodies) else() set(conventions_test_name conventions_function_bodies_shard_${shard}) endif() add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/6) + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12) set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30) endforeach() add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} @@ -135,8 +157,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 +293,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 6b413a473c..1f7568da8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 1338fe31b9..237e9166d9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -680,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]; @@ -918,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; @@ -1369,10 +1380,6 @@ struct NpUniversalDataSystemMemoryStat { std::size_t current_inuse_size; }; -struct NpUniversalDataSystemEvent {}; -struct NpUniversalDataSystemEventPropertyObject {}; -struct NpUniversalDataSystemEventPropertyArray {}; - struct NpUniversalDataSystemStorageStat { std::size_t in_events; std::size_t out_events; @@ -1965,6 +1972,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; @@ -1993,6 +2036,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..a6de94a52f 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 @@ -183,6 +185,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTraceThread.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ThreadPriority.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp @@ -204,6 +207,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 +320,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 +352,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 +385,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) @@ -457,6 +495,7 @@ add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL tests/ShaderMemory.cpp Execution/src/ShaderMemory.cpp Execution/src/GuestMemory.cpp + Execution/src/PerformanceTraceThread.cpp ) target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driver libc) @@ -472,6 +511,34 @@ target_link_libraries(agc_vertex_input_tests PRIVATE shader_recompiler_agc_drive set_target_properties(agc_vertex_input_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_vertex_input_tests) +add_test_executable(agc_guarded_memory_loads_tests EXCLUDE_FROM_ALL tests/GuardedMemoryLoads.cpp) +target_link_libraries(agc_guarded_memory_loads_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_guarded_memory_loads_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_guarded_memory_loads_tests) +if(BUILD_TESTING) + set_target_properties(agc_guarded_memory_loads_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_guarded_memory_loads COMMAND agc_guarded_memory_loads_tests) + set_tests_properties(agc_guarded_memory_loads PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_guarded_memory_loads PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_lds_lock_budget_tests EXCLUDE_FROM_ALL tests/LdsLockBudget.cpp) +target_link_libraries(agc_lds_lock_budget_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_lds_lock_budget_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_lds_lock_budget_tests) +if(BUILD_TESTING) + set_target_properties(agc_lds_lock_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_lds_lock_budget COMMAND agc_lds_lock_budget_tests) + set_tests_properties(agc_lds_lock_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_shared_write_tests EXCLUDE_FROM_ALL tests/SharedWrites.cpp) target_include_directories(agc_shared_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel libc) @@ -481,6 +548,7 @@ configure_windows_unwind(agc_shared_write_tests) add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp + Execution/src/PerformanceTraceThread.cpp ) target_include_directories(agc_write_tracking_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_write_tracking_tests PRIVATE shader_recompiler_agc_driver libc) @@ -542,6 +610,15 @@ if(BUILD_TESTING) add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests) endif() +add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp) +target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_robust_buffer_access_tests) +if(BUILD_TESTING) + set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests) +endif() + add_test_executable(agc_device_fault_tests EXCLUDE_FROM_ALL tests/DeviceFault.cpp) target_include_directories(agc_device_fault_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_device_fault_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -750,6 +827,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 +866,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 +874,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() @@ -1003,6 +1105,7 @@ add_custom_command( add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp + tests/HostImportLifetime.cpp tests/ResidentPresent.cpp ${agcTriangleHeaders} ${agcSampleLodShaderHeader} @@ -1029,6 +1132,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) @@ -1051,7 +1159,13 @@ if(BUILD_TESTING) if(WIN32) add_test(NAME agc_import_watch_other_family COMMAND agc_driver_recorder_tests --import-watch-other-family-only) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) - set_tests_properties(agc_import_watch agc_import_watch_other_family PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) + add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) + add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) + add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) + set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) @@ -1074,6 +1188,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) @@ -1166,10 +1286,28 @@ if(BUILD_TESTING) set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only) set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only) + set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) - set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only) + add_test(NAME agc_driver_host_import_mutation_race COMMAND agc_driver_recorder_tests --host-import-mutation-race-only) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) + add_test(NAME agc_driver_unit_shadow_budget COMMAND agc_driver_recorder_tests --unit-shadow-only) + set_tests_properties(agc_driver_unit_shadow PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) @@ -1186,6 +1324,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 +1430,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 +1546,33 @@ 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(ANYPS5_ENABLE_SPIRV_TOOLS) + target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1) + target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static) +endif() + +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 +1580,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() @@ -1414,10 +1631,16 @@ target_include_directories(agc_driver_reversed_order_blend_tests PRIVATE $) +target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $) + +target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel) + 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 +1661,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 $) @@ -1447,6 +1674,8 @@ target_link_libraries(agc_driver_interpolation_modes_tests PRIVATE shader_recomp target_include_directories(agc_driver_prepared_unregistered_graphics_tests PRIVATE $) +target_link_libraries(agc_driver_guest_memory_lifetime_tests PRIVATE libkernel) + target_include_directories(agc_driver_prepared_shaders_tests PRIVATE $ ) @@ -1498,8 +1727,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,11 +1750,28 @@ 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 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_tests_properties(agc_driver_registration_trace PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30) + set_property(TEST agc_driver_registration_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") + add_test(NAME agc_driver_registration_trace_disabled COMMAND ${CMAKE_COMMAND} -E env + --unset=APS5_TRACE_FRAMES --unset=APS5_TRACE_SECONDS --unset=APS5_TRACE_SUMMARY_EVERY + $ disabled) + set_tests_properties(agc_driver_registration_trace_disabled 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_registration_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement) @@ -1536,10 +1784,26 @@ 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:$") endif() + add_test(NAME agc_driver_lds_lock_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --lock-budget) + set_tests_properties(agc_driver_lds_lock_budget 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_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" @@ -1626,6 +1890,48 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set(agcGuestMemoryLifetimeTests agc_driver_guest_memory_lifetime) + foreach(mode IN ITEMS copy direct locked heap heap_uncached) + set(testName agc_driver_guest_memory_lifetime_${mode}) + if(mode STREQUAL "heap_uncached") + add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests heap) + else() + add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests ${mode}) + endif() + list(APPEND agcGuestMemoryLifetimeTests ${testName}) + endforeach() + set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=4" + SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_guest_memory_lifetime_heap_uncached PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=0") + if(WIN32) + set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND Python3_Interpreter_FOUND) + add_library(agc_guest_memory_lifetime_fixture SHARED tests/GuestMemoryLifetimeFixture.cpp) + target_include_directories(agc_guest_memory_lifetime_fixture PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include) + target_link_libraries(agc_guest_memory_lifetime_fixture PRIVATE libSceAgcDriver SDL2-static libc) + add_dependencies(agc_guest_memory_lifetime_fixture relinker libkernel) + set_target_properties(agc_guest_memory_lifetime_fixture PROPERTIES + OUTPUT_NAME GuestMemoryFixture SUFFIX ".prx" CXX_EXTENSIONS OFF + BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN") + add_test(NAME agc_driver_guest_memory_lifetime_converted COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_guest_memory_lifetime.py + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests + $ + $ + $ + $ + $) + set_tests_properties(agc_driver_guest_memory_lifetime_converted PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000" SKIP_RETURN_CODE 77 TIMEOUT 30) + endif() set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flatAperturesPart RANGE 13) @@ -1657,13 +1963,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 +2023,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 +2060,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 +2068,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 +2083,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 +2095,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 +2220,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 +2712,22 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_constant_select_tests EXCLUDE_FROM_ALL tests/ConstantSelect.cpp) +target_link_libraries(agc_constant_select_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_constant_select_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_constant_select_tests) +if(BUILD_TESTING) + set_target_properties(agc_constant_select_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_constant_select COMMAND agc_constant_select_tests) + if(WIN32) + set_tests_properties(agc_constant_select PROPERTIES ENVIRONMENT_MODIFICATION + "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/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 434f0d992b..84c2a17592 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -75,6 +75,7 @@ public: }; DispatchCacheFootprint DispatchCacheSize(); static std::uint64_t DispatchCacheBytes(); + std::shared_ptr RegisteredShaders(); private: friend class SampledReadScope; @@ -102,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; @@ -111,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(); @@ -142,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); @@ -348,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; 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/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 2388684997..21d42f766b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -26,6 +26,7 @@ struct Commitment { bool whole = false; }; Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable = false); +void CopyGuestBytes(void* destination, const void* source, std::size_t bytes); void Read(std::uint64_t address, std::span destination, std::size_t alignment = 1); // Variants for resources in heaps the guest commits on demand (partially resident textures, GPU // scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored. @@ -60,6 +61,7 @@ void WriteChanged(std::uint64_t address, std::span current, std // UnchangedSince is always false, so callers fall back to comparing bytes. bool WriteWatched(); void Unwatch(std::uint64_t address, std::size_t bytes); +void Rewatch(std::span> ranges); bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes)); bool Watched(std::uint64_t address, std::size_t bytes); 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/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 2a5314f9ff..5c7fb7d2a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -21,6 +21,13 @@ #include #include +namespace AgcDriver { +class TraceThread; +} + +extern "C" bool AgcDriverTraceRecording_nid_postfix(); +extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix(); + namespace AgcDriver { class VulkanDevice; @@ -532,23 +539,26 @@ public: } static bool Recording() { - return recording.load(std::memory_order_relaxed); + return AgcDriverTraceRecording_nid_postfix(); } static TraceThread* Thread() { - return local; + return AgcDriverTraceThread_nid_postfix(); } static void StopOutput(); private: + friend bool ::AgcDriverTraceRecording_nid_postfix(); + friend TraceThread* ::AgcDriverTraceThread_nid_postfix(); + static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present); static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device); - inline static std::atomic recording{false}; + static std::atomic recording; inline static std::atomic buffering{false}; inline static std::atomic presentCount{0}; - inline static thread_local TraceThread* local = nullptr; + static thread_local TraceThread* local; inline static std::mutex gpuMutex; inline static std::vector gpuRanges; inline static std::uint64_t gpuLost = 0; 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/RobustBufferAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp new file mode 100644 index 0000000000..a180b23104 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP + +#include + +namespace AgcDriver { + +inline bool RobustBufferAccess2Usable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 87800734a3..cc6d4a0e28 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; @@ -45,9 +55,11 @@ public: bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; std::optional GeometryLimits() const; + std::uint32_t MaxFragmentInputComponents() const; 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 +70,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 +186,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 +249,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 +337,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 045daf3d24..4b67aa557c 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); @@ -244,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(); @@ -361,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); @@ -372,6 +508,19 @@ void Driver::dispatch(QueueState& queue, std::span packet, bool writersNoted = false; const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + std::optional depthPlaneClear; + if (indirectArguments == 0 && !partialLastGroup) { + if (const auto clear = Graphics::MatchStorageImageClear(std::span(snapshot.code).subspan(codeOffset), userData, compute, groups)) { + const auto clearAddress = Graphics::StorageImageClearAddress(*clear); + if (clearAddress != 0 && Graphics::DepthSurfaceAt(clearAddress)) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); + std::lock_guard clearLock(GuestMemory::GpuMutex()); + recordLabelsForPacket(localDevice.get(), submission.queue); + depthPlaneClear.emplace(*clear); + } + } + } + for (;;) { std::shared_ptr prepared; 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..71066bbe9a 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; @@ -400,8 +454,18 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (graphics.rectList) buildRectList(); if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); - results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), localDevice->GeometryLimits())); - stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); + const auto geometryLimits = localDevice->GeometryLimits(); + if (geometryLimits || !localDevice->Target().tessellation) { + results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); + stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); + } else { + require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart"); + auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents()); + const auto first = results.size(); + results.push_back(std::move(patches.control)); + results.push_back(std::move(patches.evaluation)); + stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[first], 0}, {Stage::TessellationEvaluation, &results[first + 1], 0}}); + } } std::vector snapshots; const auto snapshot = [&] { 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/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 8fdd044922..5a5ff651f3 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); } @@ -66,8 +73,7 @@ bool StackHoldsImport() { const auto low = bounds.low; const auto high = bounds.high; #endif - const volatile unsigned char marker = 0; - const auto frame = reinterpret_cast(&marker); + const auto frame = reinterpret_cast(__builtin_frame_address(0)); return frame >= low && frame < high && frame - low >= InlineImportStackBytes; } @@ -112,18 +118,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 +229,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..2a654e0e53 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) { @@ -1194,7 +1245,16 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod return prepared; } +namespace { + +void BindTitleThread() { + if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("title", TraceThread::DefaultCapacity / 8); +} + +} + void Driver::ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.ResolveGraphicsStagesAbi"); ShaderPreparationTransaction transaction; @@ -1271,6 +1331,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std } void Driver::ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.ResolveAbi"); ShaderPreparationTransaction transaction; @@ -1342,6 +1403,7 @@ std::uint64_t NullPixelProgramAddress() { } void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.ResolveGraphicsAbi"); CheckFailure(); @@ -1462,7 +1524,13 @@ 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) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.Register"); ShaderPreparationTransaction transaction; @@ -1514,7 +1582,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..eeea635dff 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" @@ -657,8 +658,7 @@ bool onOwnLiveStack(std::uintptr_t address, std::size_t bytes) { } }; thread_local const Bounds bounds; - const volatile unsigned char marker = 0; - const auto frame = reinterpret_cast(&marker); + const auto frame = reinterpret_cast(__builtin_frame_address(0)); return bounds.high != 0 && frame >= bounds.low && frame < bounds.high && address >= frame && address < bounds.high && bytes <= bounds.high - address; } @@ -918,6 +918,32 @@ struct WriteTracker { } } + bool blockPartWatched(std::uint64_t first, std::uint64_t stop) const { +#ifdef _WIN32 + return covers(first, 1) || covers(stop - 1, 1); +#else + return GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); +#endif + } + + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); + if (address >= end) return; +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); + if (address >= end || !GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast(address), static_cast(end - address))) return; +#endif + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) { + const auto first = std::max(address, blockBegin(block)); + const auto stop = std::min(end, blockBegin(block) + WriteBlockBytes); + if (!blockPartWatched(first, stop)) continue; + stamp(block, stampGeneration, StampKind::Driver); + noteDriverStore(block, address, end, stampGeneration); + } + } + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { const auto begin = blockBegin(block); const auto from = static_cast(std::max(address, begin) - begin); @@ -1322,6 +1348,31 @@ void Unwatch(std::uint64_t address, std::size_t bytes) { unwatchLocked(tracker, address, bytes); } +void Rewatch(std::span> ranges) { +#ifdef _WIN32 + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched) return; + bool restored = false; + for (const auto& [begin, end] : ranges) { + if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WriteBlockBytes != 0 || (end - tracker.base) % WriteBlockBytes != 0) continue; + const auto bytes = static_cast(end - begin); + if (tracker.coverage.Covers(begin, bytes) || !walkWrites(tracker, begin, end, StampKind::Cpu) || !tracker.coverage.Restore(begin, bytes)) continue; + ++tracker.generation; + tracker.noteCpuStore(begin, end, tracker.generation); + for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) { + tracker.driverPieces.erase(block); + tracker.stamp(block, tracker.generation, StampKind::Cpu); + } + restored = true; + } + if (restored) unwatchSerial.fetch_add(1, std::memory_order_release); +#else + static_cast(ranges); +#endif +} + bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return import(); if (!staysWatched) { @@ -1447,6 +1498,7 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func ++tracker.generation; if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { tracker.noteUnstampedStore(stored.first, stored.second); + if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation); return 0; } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { @@ -1492,6 +1544,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { ++tracker.generation; if (!tracker.watched || !tracker.covers(address, bytes)) { tracker.noteUnstampedStore(address, address + bytes); + if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation); return 0; } { @@ -1931,12 +1984,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 +2007,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); @@ -2100,9 +2153,26 @@ void Read(std::uint64_t address, std::span destination, std::size_t a if (trace) std::fprintf(stderr, "[unreadable] read from +0x%llx\n", ModuleOffset(__builtin_return_address(0))); throw; } - std::memcpy(destination.data(), source, destination.size()); + CopyGuestBytes(destination.data(), source, destination.size()); } +#ifdef ANYPS5_ENABLE_ASAN +[[gnu::no_sanitize("address")]] void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { + auto* to = static_cast(destination); + const auto* from = static_cast(source); + std::size_t index = 0; + for (; index + sizeof(std::uint64_t) <= bytes; index += sizeof(std::uint64_t)) { + const std::uint64_t word = *reinterpret_cast(from + index); + std::memcpy(to + index, &word, sizeof(word)); + } + for (; index < bytes; ++index) to[index] = static_cast(from)[index]; +} +#else +void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { + std::memcpy(destination, source, bytes); +} +#endif + namespace { void copyCommitted(std::uint64_t address, std::span destination) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp new file mode 100644 index 0000000000..9ce81e5294 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp @@ -0,0 +1,16 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" + +namespace AgcDriver { + +std::atomic PerformanceTrace::recording{false}; +thread_local TraceThread* PerformanceTrace::local = nullptr; + +} + +extern "C" bool AgcDriverTraceRecording_nid_postfix() { + return AgcDriver::PerformanceTrace::recording.load(std::memory_order_relaxed); +} + +extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() { + return AgcDriver::PerformanceTrace::local; +} 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/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index ffbecd5b70..d658572a98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -219,7 +219,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val const auto index = static_cast((address % PageBytes) / sizeof(*value)); if (!page.valid.Test(index) && page.lazy) { const auto first = index / BlockWords * BlockWords; - std::memcpy(&page.words[first], reinterpret_cast((address & ~static_cast(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t)); + GuestMemory::CopyGuestBytes(&page.words[first], reinterpret_cast((address & ~static_cast(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t)); page.valid.SetBlock(index); } if (!page.valid.Test(index)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..bdbb94ebe8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,17 +1,21 @@ #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" #include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" +#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" @@ -134,6 +138,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 +155,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 +218,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 +226,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 +280,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 +292,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; @@ -284,6 +304,7 @@ struct VulkanDevice::State { bool float16DenormPreserve = false; bool demoteToHelperInvocation = false; bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; + bool robustBufferAccess2 = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -337,6 +358,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 +382,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 +695,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 +828,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 +1132,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 +1165,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 +1217,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 +1308,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 +1468,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 +1532,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 +1560,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 +1594,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 +1705,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 +1768,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 +1781,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) { @@ -1769,9 +1837,19 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); } - if (robustness2.nullDescriptor == VK_TRUE) { + VkDriverId robustnessDriver = VK_DRIVER_ID_MAX_ENUM; + if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { + VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + robustnessDriver = driver.driverID; + } + state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE && RobustBufferAccess2Usable(robustnessDriver); + if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) { + const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE; robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; - robustness2.nullDescriptor = VK_TRUE; + robustness2.nullDescriptor = nullDescriptor ? VK_TRUE : VK_FALSE; + robustness2.robustBufferAccess2 = state->robustBufferAccess2 ? VK_TRUE : VK_FALSE; robustness2.pNext = bdaFeatures.pNext; bdaFeatures.pNext = &robustness2; deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); @@ -1804,12 +1882,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 +1915,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 +2001,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 +2066,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() { @@ -2059,7 +2200,7 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spanbuffer, address - import->base, bytes, address); }; + const auto recordStore = [&] { recorder.RecordStore(import->buffer, Graphics::HostImportOffset(*import, address), bytes, address); }; // Only a write-back that can land over these bytes (a listed copied writer of the range, as // FillBuffer tests) puts the label behind the completions; any other pending completion // stores elsewhere. Debug aid: APS5_LABEL_GATE_ALL=1 gates on any completion, as before (a @@ -2260,7 +2401,7 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); Graphics::Recorder::CountBarriers(CommandClass::Fill); } - const auto offset = address - import->base; + const auto offset = Graphics::HostImportOffset(*import, address); if (uniform) { ++uniformFills; context.Resolved(&Graphics::DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, import->buffer, offset, bytes, pattern[0]); @@ -2527,7 +2668,7 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st Graphics::Recorder::CountBarriers(CommandClass::Copy); } const auto gpuTiming = recorder.BeginGpuTiming(programAddress); - const VkBufferCopy region{source - sourceImport->base, destination - destinationImport->base, bytes}; + const VkBufferCopy region{Graphics::HostImportOffset(*sourceImport, source), Graphics::HostImportOffset(*destinationImport, destination), bytes}; context.Resolved(&Graphics::DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, sourceImport->buffer, destinationImport->buffer, 1, ®ion); recorder.EndGpuTiming(gpuTiming, bytes); // The copied bytes are visible to everything recorded after and to the host, as after a fill. @@ -2719,20 +2860,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 +2891,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 +2900,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 +3006,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 +3061,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,8 +3502,10 @@ 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.robustBufferAccess2 = state->robustBufferAccess2; target.srgbDecodeFormats = state->srgbDecodeFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { @@ -3401,6 +3522,10 @@ std::optional VulkanDevice::GeometryLimit return ShaderRecompiler::GeometryStageLimits{limits.maxGeometryInputComponents, limits.maxGeometryOutputComponents, limits.maxGeometryOutputVertices, limits.maxGeometryTotalOutputComponents, limits.maxFragmentInputComponents}; } +std::uint32_t VulkanDevice::MaxFragmentInputComponents() const { + return state->properties.limits.maxFragmentInputComponents; +} + bool VulkanDevice::PrimitiveListRestart() const { return state->primitiveListRestart; } @@ -3465,6 +3590,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 +3633,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 +3651,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 +3706,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 +3739,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 +3787,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 +3820,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 +3987,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 +4194,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 +4272,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 +4305,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 +4350,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 +4556,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 +4609,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); @@ -4377,7 +4623,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { const auto gpuTiming = recorder.BeginGpuTiming(record.programAddress != 0 ? record.programAddress : record.shader->program->variantId); if (state->checkpointMarker != nullptr) state->checkpointMarker(commands, reinterpret_cast(static_cast(record.programAddress))); if (argumentImport != nullptr) { - context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, argumentImport->buffer, arguments - argumentImport->base); + context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, argumentImport->buffer, Graphics::HostImportOffset(*argumentImport, arguments)); } else { const auto* limit = state->properties.limits.maxComputeWorkGroupCount; if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) { @@ -4436,11 +4682,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 +4762,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 +4774,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 +4784,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 +4804,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 +4824,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 +4846,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 +4899,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 +4907,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 +4973,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 +4996,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 +5018,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 +5072,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 +5141,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..8f6edf1466 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); @@ -48,6 +49,49 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip); bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage); +struct StorageImageClear { + std::array image{}; + std::array color{}; + std::uint32_t width = 0; + std::uint32_t height = 0; +}; +std::optional MatchStorageImageClear(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::array& groups); +std::uint64_t StorageImageClearAddress(const StorageImageClear& clear); +struct DepthPlaneOverwrite { + std::array image{}; + GuestTextureResource view{}; + std::uint32_t bits = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t raw = 0; + std::uint64_t colorAddress = 0; + std::uint32_t colorBits = 0; + float depth = 0.0f; +}; +class DepthPlaneClear { +public: + explicit DepthPlaneClear(const StorageImageClear& clear); + ~DepthPlaneClear(); + DepthPlaneClear(const DepthPlaneClear&) = delete; + DepthPlaneClear& operator=(const DepthPlaneClear&) = delete; + bool Active() const { return installed; } + +private: + DepthPlaneOverwrite value; + DepthPlaneOverwrite* previous = nullptr; + bool installed = false; +}; +struct DepthClearStatistics { + std::uint64_t matched = 0; + std::uint64_t taken = 0; + std::uint64_t copiesSkipped = 0; + std::uint64_t clears = 0; + std::uint64_t bytesSkipped = 0; + std::array declined{}; +}; +DepthClearStatistics DepthClearCounts(); +void SetDepthPlaneClearForTest(bool enabled); + } #endif 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..9622fb93a1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -40,8 +40,13 @@ struct HostImport { bool joined = false; std::shared_ptr chunk; mutable std::uint64_t lastUse = 0; + std::uint64_t lead = 0; }; +inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t address) { + return address - import.base + import.lead; +} + enum class ImportWatch : std::uint8_t { Watch, Unwatch }; struct ImportProbe { @@ -63,14 +68,26 @@ ImportWatch PrepareImportWatch(const Context& context); void SetImportWatch(const Context& context, ImportWatch watch); // The host import of the registered allocation containing [address, address + bytes), made on demand -// (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in -// the import's buffer. +// (alignment and budget permitting), or null. Bytes at `address` are at `HostImportOffset(*import, +// address)` in the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +const HostImport* 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); +void SetHostImportDeferredSettles(bool enabled); std::uint64_t HostImportLiveBytes(); +struct HostImportSettleCounts { + std::uint64_t submitted = 0; + std::uint64_t waited = 0; + std::uint64_t rewatched = 0; + std::uint64_t parked = 0; + std::uint64_t unmapWaits = 0; +}; +HostImportSettleCounts HostImportSettles(); +// 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 +391,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 +446,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..078e3f8818 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; @@ -119,6 +134,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); +bool BarycentricPatches(const State& state, std::span shaders); bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); @@ -126,7 +142,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 409580699e..a2a248e09f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace AgcDriver { class VulkanDevice; } @@ -28,10 +29,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 +45,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 +102,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 +127,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 +179,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 +201,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); } @@ -227,6 +247,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; @@ -281,7 +302,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; @@ -339,12 +360,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); @@ -376,6 +400,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). @@ -487,6 +512,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; @@ -531,6 +557,7 @@ private: std::vector textureBaseLevel; std::vector textureSrgb; std::vector> storageTextures; + std::vector multisampleStorageViews; std::vector storageMips; std::vector storageKeys; std::vector storageFirstLayer; @@ -629,13 +656,20 @@ public: static std::uint64_t Touches(); void Clear(); std::size_t Size() const; + std::size_t Capacity() const; + void SetBase(std::size_t entries, std::size_t shrinkWindow); std::uint64_t TableElements() const; static std::uint64_t TableElementBudget(); + static constexpr std::size_t InitialEntries = 1024; + static constexpr std::size_t MaximumEntries = 16384; private: void erase(const Key& key); void noteMiss(const Key& key); void dropReleasedTextures(std::vector>* evicted); + void noteEvicted(const Key& key); + void noteReturn(const Key& key); + void noteUse(const Key& key); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { std::uint64_t hash = 14695981039346656037ull; @@ -646,6 +680,20 @@ private: mutable std::mutex mutex; std::list>> entries; std::unordered_map index; + std::size_t capacity = 0; + bool fixedCapacity = false; + std::list evictedOrder; + std::unordered_map evictedKeys; + std::size_t evictionsSinceCheck = 0; + std::size_t returnsSinceCheck = 0; + std::unordered_set usedKeys; + std::size_t usesSinceCheck = 0; + static constexpr std::size_t InitialShrinkWindow = 8; + static constexpr std::size_t MaximumShrinkWindow = 1024; + std::size_t baseEntries = InitialEntries; + std::size_t baseShrinkWindow = InitialShrinkWindow; + std::size_t shrinkWindow = InitialShrinkWindow; + bool shrankLast = false; }; // The process-wide cache the device and the draw path share (keys name the device; a device clears 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 3ee21eb426..0fbde65cbc 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); @@ -188,6 +189,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/TextureSwizzleEquations.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp index b77267d50a..d408819b49 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp @@ -84,6 +84,11 @@ inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = { {0x109u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}}, {0x109u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}}, {0x109u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}}, + {0x115u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x20002020u, 0x10010004u, 0x04008010u, 0x08004008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x20002010u, 0x10010002u, 0x04008008u, 0x08004004u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {0x119u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000010u, 0x10000000u, 0x00010000u, 0x00000020u}}, {0x119u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000008u, 0x10000000u, 0x00010000u, 0x00000010u}}, {0x119u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}}, 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..51195023df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -234,7 +234,7 @@ void TraceKeyStore(const char* path, std::uint64_t begin, std::size_t count) { bool StoreUncompressedOnGpu(const Context& context, Recorder& recorder, std::uint64_t begin, std::size_t count) { const auto* import = HostImportFor(context, begin, count); if (import == nullptr) return false; - const VkDeviceSize first = begin - import->base; + const VkDeviceSize first = HostImportOffset(*import, begin); const VkDeviceSize last = first + count; const VkDeviceSize fillBegin = std::min((first + 3) & ~VkDeviceSize{3}, last); const VkDeviceSize fillEnd = std::max(last & ~VkDeviceSize{3}, fillBegin); @@ -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..0ac8d9b104 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -119,6 +120,104 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge return state; } +std::atomic depthPlaneClearEnabled{true}; + +enum DepthClearDecline : std::size_t { DeclineLayers, DeclineFormat, DeclineValue, DeclineColor, DeclinePending, DeclineCoverage, DeclineSize, DeclineCount }; +static_assert(DeclineCount <= 8, "DepthClearStatistics::declined holds eight reasons"); + +struct DepthClearCounters { + std::atomic matched{0}; + std::atomic taken{0}; + std::atomic copiesSkipped{0}; + std::atomic clears{0}; + std::atomic bytesSkipped{0}; + std::array, 8> declined{}; +}; + +DepthClearCounters& depthClearCounters() { + static DepthClearCounters counters; + return counters; +} + +thread_local DepthPlaneOverwrite* currentOverwrite = nullptr; + +std::optional decodeOverwrite(const StorageImageClear& clear) { + auto& counters = depthClearCounters(); + ++counters.matched; + const auto decline = [&](DepthClearDecline why) -> std::optional { + ++counters.declined[why]; + return std::nullopt; + }; + DepthPlaneOverwrite overwrite; + overwrite.image = clear.image; + try { + overwrite.view = DecodeTextureResource(clear.image); + } catch (...) { + return decline(DeclineFormat); + } + const auto& view = overwrite.view; + const bool oneLayer = view.dimension == TextureDimension::k2D || (view.dimension == TextureDimension::k2DArray && view.baseArray == 0 && view.depthOrLastArray == 0); + if (!oneLayer || view.baseLevel != 0) return decline(DeclineLayers); + VkFormat format = VK_FORMAT_UNDEFINED; + try { + format = ResolveTextureFormat(view.format); + } catch (...) { + return decline(DeclineFormat); + } + enum class Kind { Unorm16, Uint16, Float32, Uint32 }; + Kind kind; + switch (format) { + case VK_FORMAT_R16_UNORM: kind = Kind::Unorm16; break; + case VK_FORMAT_R16_UINT: kind = Kind::Uint16; break; + case VK_FORMAT_R32_SFLOAT: kind = Kind::Float32; break; + case VK_FORMAT_R32_UINT: + case VK_FORMAT_R32_SINT: kind = Kind::Uint32; break; + default: return decline(DeclineFormat); + } + const std::uint64_t base = clear.color[0] | (static_cast(clear.color[1] & 0xffffu) << 32u); + const std::uint64_t stride = (clear.color[1] >> 16u) & 0x3fffu; + const std::uint64_t extent = stride == 0 ? clear.color[2] : static_cast(clear.color[2]) * stride; + if (base == 0 || (base & 3u) != 0u || extent < 16u || !GuestMemory::Accessible(reinterpret_cast(base), 16)) return decline(DeclineColor); + if (const auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(base, 16)) return decline(DeclinePending); + const std::pair colorRange{base, base + 16u}; + if (StorageTexture::AnyPendingOverlaps(std::span(&colorRange, 1))) return decline(DeclinePending); + std::uint32_t c0 = 0; + try { + GuestMemory::Read(base, std::as_writable_bytes(std::span(&c0, 1)), 4); + } catch (...) { + return decline(DeclineColor); + } + switch (kind) { + case Kind::Unorm16: + if (c0 == 0u || c0 == 0x80000000u) overwrite.raw = 0u; + else if (c0 == 0x3f800000u) overwrite.raw = 0xffffu; + else return decline(DeclineValue); + overwrite.bits = 16u; + overwrite.depth = static_cast(overwrite.raw) / 65535.0f; + break; + case Kind::Uint16: + if (c0 != 0u && c0 != 0xffffu) return decline(DeclineValue); + overwrite.raw = c0; + overwrite.bits = 16u; + overwrite.depth = static_cast(overwrite.raw) / 65535.0f; + break; + case Kind::Float32: + case Kind::Uint32: { + const auto depth = std::bit_cast(c0); + if (c0 != 0u && !(std::isnormal(depth) && depth > 0.0f && depth <= 1.0f)) return decline(DeclineValue); + overwrite.raw = c0; + overwrite.bits = 32u; + overwrite.depth = depth; + break; + } + } + overwrite.colorAddress = base; + overwrite.colorBits = c0; + overwrite.width = clear.width; + overwrite.height = clear.height; + return overwrite; +} + class DepthSurface { public: DepthSurface(const Context& context, const DepthTarget& target, DepthSamplingState sampling = {}) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)), sampling(std::move(sampling)) { @@ -249,6 +348,14 @@ public: } void RecordClear(VkImageAspectFlags aspects) { + const float depth = depthFill.value_or(clearDepth); + const std::uint8_t stencil = stencilFill.value_or(clearStencil); + depthFill.reset(); + stencilFill.reset(); + RecordClearValue(aspects, depth, stencil); + } + + void RecordClearValue(VkImageAspectFlags aspects, float depth, std::uint8_t stencil) { written = ++depthWrites; auto* recorder = RecorderFor(context); std::unique_ptr batch; @@ -257,7 +364,7 @@ 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{depth, stencil}; 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 +374,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 +409,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 +429,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 +465,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 +494,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 +506,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 +514,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 +525,8 @@ 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); + auto* overwrite = stencil ? nullptr : PlaneOverwritten(words); 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) { @@ -404,15 +534,67 @@ public: if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); } - copyPlane(plane, plane.storage, stencil, true); + if (overwrite != nullptr) { + ++depthClearCounters().copiesSkipped; + } else { + copyPlane(plane, plane.storage, stencil, true); + } return plane.storage; } + bool OverwritesWholePlane(const DepthPlaneOverwrite& overwrite) const { + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return overwrite.bits == (d16 ? 16u : 32u) && overwrite.width >= target.extent.width && overwrite.height >= target.extent.height; + } + + DepthPlaneOverwrite* PlaneOverwritten(std::span words) const { + auto* overwrite = currentOverwrite; + if (overwrite == nullptr || words.size() < overwrite->image.size() || !std::equal(overwrite->image.begin(), overwrite->image.end(), words.begin())) return nullptr; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + auto& declined = depthClearCounters().declined; + if (overwrite->bits != (d16 ? 16u : 32u)) { + ++declined[DeclineSize]; + return nullptr; + } + if (overwrite->width < target.extent.width || overwrite->height < target.extent.height) { + ++declined[DeclineCoverage]; + return nullptr; + } + return overwrite; + } + + bool StoreOverwrite(const StorageTexture& mirror) { + auto* overwrite = currentOverwrite; + if (overwrite == nullptr || !OverwritesWholePlane(*overwrite)) return false; + const auto& described = mirror.Descriptor(); + const auto& view = overwrite->view; + if (described.baseAddress != view.baseAddress || described.format != view.format || described.tileMode != view.tileMode || described.dimension != view.dimension) return false; + std::uint32_t current = 0; + try { + GuestMemory::Read(overwrite->colorAddress, std::as_writable_bytes(std::span(¤t, 1)), 4); + } catch (...) { + return false; + } + if (current != overwrite->colorBits) return false; + auto* recorder = RecorderFor(context); + Require(recorder != nullptr, "depth surface storage clears need the recorder of the surface's device"); + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytes = static_cast(target.extent.width) * target.extent.height * (d16 ? 2u : 4u); + recorder->Commands(); + const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + RecordClearValue(VK_IMAGE_ASPECT_DEPTH_BIT, overwrite->depth, clearStencil); + recorder->EndGpuTiming(timing, bytes); + auto& counters = depthClearCounters(); + ++counters.clears; + counters.bytesSkipped += 2u * bytes; + return true; + } + bool StoreBack(const StorageTexture& candidate) { for (const bool stencil : {false, true}) { auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage.get() == &candidate) { - copyPlane(plane, plane.storage, stencil, false); + if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; return true; } @@ -481,11 +663,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 +804,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 +1130,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 +1318,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 +1406,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(); @@ -1171,6 +1449,64 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } +std::optional MatchStorageImageClear(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::array& groups) { + static constexpr std::array kernel{0xd7460004u, 0x0401060cu, 0xf4280204u, 0xfa000000u, 0xd7460005u, 0x0405060du, 0x7e0c020eu, 0xbf8cc07fu, 0x7e000208u, 0x7e020209u, 0x7e04020au, 0x7e06020bu, 0xf0200f28u, 0x00000004u, 0xbf810000u}; + if (code.size() < kernel.size() || code[0] != kernel[0] || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if (userData.size() != 12u) return std::nullopt; + if (compute.numThreads != std::array{8u, 8u, 1u}) return std::nullopt; + if (!compute.groupIdEnable[0] || !compute.groupIdEnable[1] || !compute.groupIdEnable[2]) return std::nullopt; + if (compute.threadIdComponentCount < 2u) return std::nullopt; + std::array covered{}; + for (std::size_t axis = 0; axis < 3u; ++axis) covered[axis] = compute.partialThreads[axis] != 0u ? compute.partialThreads[axis] : static_cast(groups[axis]) * compute.numThreads[axis]; + if (covered[0] == 0u || covered[1] == 0u || covered[2] != 1u) return std::nullopt; + StorageImageClear clear; + std::copy_n(userData.begin(), clear.image.size(), clear.image.begin()); + std::copy_n(userData.begin() + static_cast(clear.image.size()), clear.color.size(), clear.color.begin()); + clear.width = static_cast(std::min(covered[0], std::numeric_limits::max())); + clear.height = static_cast(std::min(covered[1], std::numeric_limits::max())); + return clear; +} + +std::uint64_t StorageImageClearAddress(const StorageImageClear& clear) { + try { + return DecodeTextureResource(clear.image).baseAddress; + } catch (...) { + return 0u; + } +} + +DepthPlaneClear::DepthPlaneClear(const StorageImageClear& clear) { + if (!depthPlaneClearEnabled.load(std::memory_order_relaxed)) return; + const auto decoded = decodeOverwrite(clear); + if (decoded) { + value = *decoded; + previous = currentOverwrite; + currentOverwrite = &value; + installed = true; + ++depthClearCounters().taken; + } +} + +DepthPlaneClear::~DepthPlaneClear() { + if (installed) currentOverwrite = previous; +} + +DepthClearStatistics DepthClearCounts() { + const auto& counters = depthClearCounters(); + DepthClearStatistics statistics; + statistics.matched = counters.matched.load(); + statistics.taken = counters.taken.load(); + statistics.copiesSkipped = counters.copiesSkipped.load(); + statistics.clears = counters.clears.load(); + statistics.bytesSkipped = counters.bytesSkipped.load(); + for (std::size_t reason = 0; reason < statistics.declined.size(); ++reason) statistics.declined[reason] = counters.declined[reason].load(); + return statistics; +} + +void SetDepthPlaneClearForTest(bool enabled) { + depthPlaneClearEnabled.store(enabled, std::memory_order_relaxed); +} + std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments) { if (userData.size() != 6u || packet.size() < 5u || indirectArguments != 0u || compute.PartialGroups() || !compute.groupIdEnable[0] || compute.threadIdComponentCount == 0u || compute.threadIdComponentCount > 3u) return 0u; if (packet[1] == 0u || packet[1] > 0x400000u || packet[2] != 1u || packet[3] != 1u || (packet[4] & 0x23u) != 1u) return 0u; @@ -1196,7 +1532,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 +1580,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 +1596,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 +1660,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 +1687,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 +1736,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 +1755,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 +1792,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..8ef1050892 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -258,7 +258,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo recorder.FlushKeyStoresOverlapping(address, bytes); const auto commands = recorder.Commands(); recorder.Keep(resident); - context.detiler->DispatchCmaskClear(commands, import.buffer, address - import.base, bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); + context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); recorder.NotePendingWrite(address, bytes); proveCmask(address, bytes, GuestMemory::MarkWritten(address, bytes)); } @@ -289,7 +289,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con Require(color.dccAddress == 0, "CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys"); const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; - const bool filled = state == CmaskState::Cleared && resident != nullptr && clearToTexel(*resident, texel, color.elementBytes, refusal); + const bool filled = state == CmaskState::Cleared && color.mipCount == 1 && resident != nullptr && clearToTexel(*resident, texel, color.elementBytes, refusal); if (!filled) { storeCmaskClearTexels(color, layout, state == CmaskState::Cleared ? nullptr : &cleared, texel); if (resident != nullptr) resident->Refresh(); @@ -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,18 +951,26 @@ 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(); if (std::any_of(drawWriters->begin(), drawWriters->end(), overlaps)) return nullptr; const auto* import = HostImportFor(context, address, bytes); - if (import == nullptr || (address - import->base) % alignment != 0) return nullptr; + if (import == nullptr || HostImportOffset(*import, address) % 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; @@ -1114,7 +1150,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P } if (import != nullptr) { inputs.indexHandle = import->buffer; - inputs.indexOffset = draw.indexAddress - import->base; + inputs.indexOffset = HostImportOffset(*import, draw.indexAddress); inputs.inPlaceReads.emplace_back(draw.indexAddress, draw.indexAddress + indexBytes); } else { inputs.indices = std::move(copy.buffer); @@ -1180,9 +1216,10 @@ 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); + rangeOffsets.push_back(HostImportOffset(*import, begin)); inputs.inPlaceReads.emplace_back(begin, end); continue; } @@ -1348,8 +1385,8 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const else context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, draw.indexCount, draw.instanceCount, draw.firstVertex, draw.firstInstance); } else if (indirect->path == IndirectDrawPath::Gpu) { if (args->countIndirect) { - if (draw.indexed) context.Function("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, args->countAddress - indirect->countImport->base, args->count, args->stride); - else context.Function("vkCmdDrawIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, args->countAddress - indirect->countImport->base, args->count, args->stride); + if (draw.indexed) context.Function("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride); + else context.Function("vkCmdDrawIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride); } else if (args->count <= 1 || context.multiDrawIndirect) { if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexedIndirect, "vkCmdDrawIndexedIndirect")(commands, argumentBuffer, argumentOffset, args->count, args->stride); else context.Resolved(&DeviceFunctions::cmdDrawIndirect, "vkCmdDrawIndirect")(commands, argumentBuffer, argumentOffset, args->count, args->stride); @@ -1397,7 +1434,7 @@ bool recordIndirectArguments(const Context& context, VkCommandBuffer commands, R if (args->countIndirect) recorder->NotePendingRead(args->countAddress, 4, Recorder::ReadKind::Indirect); } argumentBuffer = indirect.argumentImport->buffer; - argumentOffset = args->arguments - indirect.argumentImport->base; + argumentOffset = HostImportOffset(*indirect.argumentImport, args->arguments); const bool rewritten = args->vertexRule == Rule::Constant || args->instanceRule == Rule::Constant; if (!rewritten) return false; const auto bytes = static_cast(args->RangeBytes()); @@ -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 @@ -1676,7 +1717,7 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c } const auto* imported = HostImportFor(context, begin, bytes); Require(imported != nullptr, "capture input has no host import"); - addSample(begin, bytes, imported->buffer, begin - imported->base, reinterpret_cast(begin)); + addSample(begin, bytes, imported->buffer, HostImportOffset(*imported, begin), reinterpret_cast(begin)); } CaptureTrace::Log("input-capture draw=%llu batch=%llu target=%llx ranges=%zu bytes=%zu", draw, batch, static_cast(target), samples.size(), total); if (samples.empty()) return; @@ -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 @@ -2078,6 +2135,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages && binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue; for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { const auto words = std::span(binding.guestDescriptor).subspan(offset, 8u); + if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; const auto texture = DecodeTextureResource(words); Require(!DepthSurfaceAt(context, words, texture), "stencil replay refuses sampled depth-surface feedback"); } @@ -2347,6 +2405,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..6f6b03fb6e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -160,6 +160,44 @@ struct ImportFailure { GuestAllocations::Mapped ranges; }; +#ifdef _WIN32 +constexpr std::size_t settleBatchCount = 4; +constexpr std::uint32_t settleBatchCopies = 256; + +struct SettleBatch { + VkCommandBuffer commands = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + std::vector bases; + std::vector aliases; + std::vector> retired; + bool busy = false; +}; + +enum class SettlePhase : std::uint8_t { Wanted, Submitted, Settled }; + +struct PendingSettle { + SettlePhase phase = SettlePhase::Wanted; + std::size_t batch = 0; + VkDevice device = VK_NULL_HANDLE; +}; +#endif + +struct ImportSettle { + VkBuffer settleTarget = VK_NULL_HANDLE; + VkDeviceMemory settleMemory = VK_NULL_HANDLE; + VkCommandBuffer settleCommands = VK_NULL_HANDLE; + VkFence settleFence = VK_NULL_HANDLE; + PFN_vkDestroyFence destroyFence = nullptr; + PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; + PFN_vkFreeMemory settleFreeMemory = nullptr; +#ifdef _WIN32 + PFN_vkWaitForFences waitForFences = nullptr; + VkBuffer batchTarget = VK_NULL_HANDLE; + VkDeviceMemory batchMemory = VK_NULL_HANDLE; + std::array batches{}; +#endif +}; + // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -184,15 +222,17 @@ struct HostImports { bool unwatchImports = false; bool unwatchDmaBufImports = false; bool watchPrivateImports = false; + std::map settles; #ifdef _WIN32 - VkDevice settleDevice = VK_NULL_HANDLE; - VkBuffer settleTarget = VK_NULL_HANDLE; - VkDeviceMemory settleMemory = VK_NULL_HANDLE; - VkCommandBuffer settleCommands = VK_NULL_HANDLE; - VkFence settleFence = VK_NULL_HANDLE; - PFN_vkDestroyFence destroyFence = nullptr; - PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; - PFN_vkFreeMemory settleFreeMemory = nullptr; + std::map pendingSettles; + std::vector settleWanted; + std::vector settleReady; + std::chrono::steady_clock::time_point settlePolled{}; + std::uint64_t settlesSubmitted = 0; + std::uint64_t settlesWaited = 0; + std::uint64_t settlesRewatched = 0; + std::uint64_t settlesParked = 0; + std::uint64_t settleUnmapWaits = 0; #endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; @@ -203,6 +243,57 @@ HostImports& Imports() { return imports; } +#ifdef _WIN32 +void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { + if (batch.busy && settle.waitForFences != nullptr) static_cast(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); +} + +bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { + return batch.busy && settle.waitForFences != nullptr && settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 0) == VK_TIMEOUT; +} + +SettleBatch* forgetSettle(HostImports& state, std::uint64_t base) { + const auto found = state.pendingSettles.find(base); + if (found == state.pendingSettles.end()) return nullptr; + SettleBatch* running = nullptr; + if (const auto settle = state.settles.find(found->second.device); found->second.phase == SettlePhase::Submitted && settle != state.settles.end()) { + auto& batch = settle->second.batches[found->second.batch]; + if (settleBatchRunning(settle->second, settle->first, batch)) running = &batch; + } + state.pendingSettles.erase(found); + return running; +} + +void drainSettles(HostImports& state) { + for (auto& [device, settle] : state.settles) { + for (auto& batch : settle.batches) { + waitSettleBatch(settle, device, batch); + for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr); + batch.aliases.clear(); + batch.retired.clear(); + batch.bases.clear(); + batch.busy = false; + } + } + state.pendingSettles.clear(); + state.settleWanted.clear(); + state.settleReady.clear(); +} + +void releaseSettleBatches(VkDevice device, ImportSettle& settle) { + for (auto& batch : settle.batches) { + waitSettleBatch(settle, device, batch); + for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr); + if (batch.fence != VK_NULL_HANDLE) settle.destroyFence(device, batch.fence, nullptr); + batch = SettleBatch{}; + } + if (settle.batchTarget != VK_NULL_HANDLE) settle.settleDestroyBuffer(device, settle.batchTarget, nullptr); + if (settle.batchMemory != VK_NULL_HANDLE) settle.settleFreeMemory(device, settle.batchMemory, nullptr); + settle.batchTarget = VK_NULL_HANDLE; + settle.batchMemory = VK_NULL_HANDLE; +} +#endif + void destroyImport(const Context& context, const HostImport& entry) { context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); @@ -239,6 +330,9 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); void retireImport(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, bool hold = true) { +#ifdef _WIN32 + auto* reading = forgetSettle(state, it->first); +#endif // Results shadowed for the import reach its (old) buffer first where the memory is still a // readable registered range (the import retires because its registration vanished or changed // size); the holder below outlives the batch that copies them. @@ -246,6 +340,12 @@ void retireImport(VkDevice device, HostImports& state, std::mapsecond.bytes; auto holder = std::make_shared(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it)); +#ifdef _WIN32 + if (reading != nullptr) { + reading->retired.push_back(holder); + ++state.settlesParked; + } +#endif auto* recorder = Recorder::Active(); if (recorder == nullptr) return; if (hold) recorder->KeepThroughHold(std::move(holder)); @@ -272,6 +372,50 @@ bool releaseParkedImports(HostImports& state, std::uint64_t begin, std::uint64_t return !released.empty(); } +#ifdef _WIN32 +bool releaseSettleRetired(HostImports& state, std::uint64_t begin, std::uint64_t end) { + std::vector> released; + { + std::lock_guard lock(state.mutex); + for (auto& [device, settle] : state.settles) { + for (auto& batch : settle.batches) { + const bool overlaps = std::any_of(batch.retired.begin(), batch.retired.end(), [&](const std::shared_ptr& holder) { return holder->node.mapped().base < end && begin < holder->node.mapped().base + holder->node.mapped().bytes; }); + if (!overlaps) continue; + if (settleBatchRunning(settle, device, batch)) { + ++state.settleUnmapWaits; + waitSettleBatch(settle, device, batch); + } + for (auto& holder : batch.retired) released.push_back(std::move(holder)); + batch.retired.clear(); + } + } + } + return !released.empty(); +} +#endif + +#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 +432,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); @@ -363,6 +511,7 @@ const char* createDmaBufImport(const Context& context, HostImport& entry, int fi #ifndef _WIN32 constexpr std::uint64_t ImportChunkBytes = std::uint64_t{64} << 20u; constexpr VkBufferUsageFlags ChunkImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; +constexpr std::uint64_t SparseBlockBytes = 65536; struct ImportChunk { VkDevice device = VK_NULL_HANDLE; @@ -466,7 +615,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 +669,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{}; @@ -537,13 +684,15 @@ VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const Host } bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { + const auto lead = offset % SparseBlockBytes; + offset -= lead; const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; if (!context.sparseBinding || entry.bytes <= ImportChunkBytes || (!ownQueue && !GuestMemory::GpuMutex().HeldByThisThread()) || udmabufDevice() < 0) return false; struct stat backing {}; if (fstat(file, &backing) != 0) throwSparseImport(entry, "fstat of the shared backing", VK_ERROR_INVALID_EXTERNAL_HANDLE); const auto chunkTypes = dmaBufBufferRequirements(context, entry, 0, ChunkImportUsage, ImportChunkBytes, nullptr).memoryTypeBits; VkBuffer buffer = VK_NULL_HANDLE; - const auto requirements = dmaBufBufferRequirements(context, entry, VK_BUFFER_CREATE_SPARSE_BINDING_BIT, SparseImportUsage, entry.bytes, &buffer); + const auto requirements = dmaBufBufferRequirements(context, entry, VK_BUFFER_CREATE_SPARSE_BINDING_BIT, SparseImportUsage, lead + entry.bytes, &buffer); const auto alignment = requirements.alignment; const bool chosen = alignment != 0 && offset % alignment == 0 && ImportChunkBytes % alignment == 0 && (chunkTypes & ~requirements.memoryTypeBits) == 0 && (requirements.memoryTypeBits & chunkTypes) != 0 && (offset + requirements.size + ImportChunkBytes - 1) / ImportChunkBytes * ImportChunkBytes <= static_cast(backing.st_size); @@ -594,9 +743,10 @@ bool createSparseDmaBufImport(const Context& context, HostImport& entry, int fil if (address == 0) fail("vkGetBufferDeviceAddressKHR", VK_ERROR_UNKNOWN); entry.buffer = buffer; entry.memory = VK_NULL_HANDLE; - entry.address = address; + entry.address = address + lead; entry.chunk = std::move(chunks); entry.dmaBuf = true; + entry.lead = lead; return true; } #endif @@ -650,59 +800,70 @@ ImportWatchRequest importWatchRequest() { } #endif -#ifdef _WIN32 +bool importsStale(const Context& context, const HostImports& state); + constexpr std::uint64_t settleFloorBytes = 0x100000; +#ifdef _WIN32 +constexpr std::uint64_t settleBlockBytes = 0x10000; +constexpr auto settlePollPeriod = std::chrono::microseconds(100); +std::atomic deferredSettles{std::getenv("APS5_NO_DEFERRED_SETTLES") == nullptr}; +#endif + +ImportSettle& settleFor(const Context& context, HostImports& state) { + auto& settle = state.settles[context.device]; + settle.destroyFence = context.Function("vkDestroyFence"); + settle.settleDestroyBuffer = context.Function("vkDestroyBuffer"); + settle.settleFreeMemory = context.Function("vkFreeMemory"); +#ifdef _WIN32 + settle.waitForFences = context.Function("vkWaitForFences"); +#endif + return settle; +} + +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) { - state.settleDevice = context.device; - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; - state.settleCommands = VK_NULL_HANDLE; - state.settleFence = VK_NULL_HANDLE; - state.destroyFence = context.Function("vkDestroyFence"); - state.settleDestroyBuffer = context.Function("vkDestroyBuffer"); - state.settleFreeMemory = context.Function("vkFreeMemory"); - } - if (state.settleTarget == VK_NULL_HANDLE) { + auto& settle = settleFor(context, state); + if (settle.settleTarget == VK_NULL_HANDLE) { VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; info.size = 256; info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - if ((result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &state.settleTarget)) != VK_SUCCESS) { - state.settleTarget = VK_NULL_HANDLE; + if ((result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &settle.settleTarget)) != VK_SUCCESS) { + settle.settleTarget = VK_NULL_HANDLE; return "vkCreateBuffer (settle)"; } VkMemoryRequirements requirements{}; - context.Function("vkGetBufferMemoryRequirements")(context.device, state.settleTarget, &requirements); + context.Function("vkGetBufferMemoryRequirements")(context.device, settle.settleTarget, &requirements); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = static_cast(std::countr_zero(requirements.memoryTypeBits)); - if ((result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &state.settleMemory)) != VK_SUCCESS || (result = context.Function("vkBindBufferMemory")(context.device, state.settleTarget, state.settleMemory, 0)) != VK_SUCCESS) { - context.Function("vkDestroyBuffer")(context.device, state.settleTarget, nullptr); - if (state.settleMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, state.settleMemory, nullptr); - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; + if ((result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &settle.settleMemory)) != VK_SUCCESS || (result = context.Function("vkBindBufferMemory")(context.device, settle.settleTarget, settle.settleMemory, 0)) != VK_SUCCESS) { + context.Function("vkDestroyBuffer")(context.device, settle.settleTarget, nullptr); + if (settle.settleMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, settle.settleMemory, nullptr); + settle.settleTarget = VK_NULL_HANDLE; + settle.settleMemory = VK_NULL_HANDLE; return "the settle target"; } VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; allocate.commandPool = context.settlePool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1; - if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &state.settleCommands)) != VK_SUCCESS) { - state.settleCommands = VK_NULL_HANDLE; + if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &settle.settleCommands)) != VK_SUCCESS) { + settle.settleCommands = VK_NULL_HANDLE; return "vkAllocateCommandBuffers (settle)"; } const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; - if ((result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &state.settleFence)) != VK_SUCCESS) { - state.settleFence = VK_NULL_HANDLE; + if ((result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &settle.settleFence)) != VK_SUCCESS) { + settle.settleFence = VK_NULL_HANDLE; return "vkCreateFence (settle)"; } } - if (state.settleCommands == VK_NULL_HANDLE || state.settleFence == VK_NULL_HANDLE) return "the settle commands"; + if (settle.settleCommands == VK_NULL_HANDLE || settle.settleFence == VK_NULL_HANDLE) return "the settle commands"; 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,26 +874,224 @@ 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; } - 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)"; +#endif + if ((result = context.Function("vkResetFences")(context.device, 1, &settle.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)"; + if ((result = context.Function("vkResetCommandBuffer")(settle.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)"; VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - if ((result = context.Function("vkBeginCommandBuffer")(state.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)"; + if ((result = context.Function("vkBeginCommandBuffer")(settle.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)"; const VkBufferCopy region{0, 0, 4}; - context.Function("vkCmdCopyBuffer")(state.settleCommands, source, state.settleTarget, 1, ®ion); - if ((result = context.Function("vkEndCommandBuffer")(state.settleCommands)) != VK_SUCCESS) return "vkEndCommandBuffer (settle)"; + context.Function("vkCmdCopyBuffer")(settle.settleCommands, source, settle.settleTarget, 1, ®ion); + if ((result = context.Function("vkEndCommandBuffer")(settle.settleCommands)) != VK_SUCCESS) return "vkEndCommandBuffer (settle)"; VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; - submit.pCommandBuffers = &state.settleCommands; - if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, state.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)"; - if ((result = context.Function("vkWaitForFences")(context.device, 1, &state.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)"; + submit.pCommandBuffers = &settle.settleCommands; + if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, settle.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)"; +#ifdef _WIN32 + ++state.settlesWaited; +#endif + if ((result = context.Function("vkWaitForFences")(context.device, 1, &settle.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)"; return nullptr; }; 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; } + +#ifdef _WIN32 +const char* prepareSettleBatch(const Context& context, ImportSettle& settle, SettleBatch& batch, VkResult& result) { + if (settle.batchTarget == VK_NULL_HANDLE) { + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = VkDeviceSize{settleBatchCount} * settleBatchCopies * 4u; + info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + if ((result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &settle.batchTarget)) != VK_SUCCESS) { + settle.batchTarget = VK_NULL_HANDLE; + return "vkCreateBuffer (settle batch)"; + } + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, settle.batchTarget, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = static_cast(std::countr_zero(requirements.memoryTypeBits)); + if ((result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &settle.batchMemory)) != VK_SUCCESS || (result = context.Function("vkBindBufferMemory")(context.device, settle.batchTarget, settle.batchMemory, 0)) != VK_SUCCESS) { + context.Function("vkDestroyBuffer")(context.device, settle.batchTarget, nullptr); + if (settle.batchMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, settle.batchMemory, nullptr); + settle.batchTarget = VK_NULL_HANDLE; + settle.batchMemory = VK_NULL_HANDLE; + return "the settle batch target"; + } + } + if (batch.commands == VK_NULL_HANDLE) { + VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + allocate.commandPool = context.settlePool; + allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocate.commandBufferCount = 1; + if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &batch.commands)) != VK_SUCCESS) { + batch.commands = VK_NULL_HANDLE; + return "vkAllocateCommandBuffers (settle batch)"; + } + } + if (batch.fence == VK_NULL_HANDLE) { + const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + if ((result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &batch.fence)) != VK_SUCCESS) { + batch.fence = VK_NULL_HANDLE; + return "vkCreateFence (settle batch)"; + } + } + return nullptr; +} + +void dropWantedSettles(HostImports& state) { + for (const auto base : state.settleWanted) { + if (const auto settle = state.pendingSettles.find(base); settle != state.pendingSettles.end() && settle->second.phase == SettlePhase::Wanted) state.pendingSettles.erase(settle); + } + state.settleWanted.clear(); +} + +void submitSettleBatch(const Context& context, HostImports& state) { + if (state.settleWanted.empty()) return; + auto& deviceSettle = settleFor(context, state); + const auto free = std::find_if(deviceSettle.batches.begin(), deviceSettle.batches.end(), [](const SettleBatch& batch) { return !batch.busy; }); + if (free == deviceSettle.batches.end()) return; + const auto index = static_cast(free - deviceSettle.batches.begin()); + auto& batch = *free; + std::unique_lock queueLock(*context.settleQueueMutex); + VkResult result = VK_SUCCESS; + const auto destroyBuffer = context.Function("vkDestroyBuffer"); + const auto fail = [&](const char* step) { + for (const auto alias : batch.aliases) destroyBuffer(context.device, alias, nullptr); + batch.aliases.clear(); + for (const auto base : batch.bases) state.pendingSettles.erase(base); + batch.bases.clear(); + dropWantedSettles(state); + std::fprintf(stderr, "[gpu] settling host imports under %llu KiB without waiting failed at %s (%d); they stay compared\n", static_cast(settleFloorBytes >> 10u), step, static_cast(result)); + }; + if (const char* step = prepareSettleBatch(context, deviceSettle, batch, result)) return fail(step); + if ((result = context.Function("vkResetFences")(context.device, 1, &batch.fence)) != VK_SUCCESS) return fail("vkResetFences (settle batch)"); + if ((result = context.Function("vkResetCommandBuffer")(batch.commands, 0)) != VK_SUCCESS) return fail("vkResetCommandBuffer (settle batch)"); + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if ((result = context.Function("vkBeginCommandBuffer")(batch.commands, &begin)) != VK_SUCCESS) return fail("vkBeginCommandBuffer (settle batch)"); + const auto copy = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); + const bool otherFamily = context.settleFamily != context.queueFamily; + const auto createBuffer = otherFamily ? context.Function("vkCreateBuffer") : nullptr; + const auto bindBufferMemory = otherFamily ? context.Function("vkBindBufferMemory") : nullptr; + std::size_t consumed = 0; + for (; consumed < state.settleWanted.size() && batch.bases.size() < settleBatchCopies; ++consumed) { + const auto base = state.settleWanted[consumed]; + const auto settle = state.pendingSettles.find(base); + if (settle == state.pendingSettles.end() || settle->second.phase != SettlePhase::Wanted) continue; + const auto import = state.imports.find(base); + if (import == state.imports.end() || !import->second.unwatched) { + state.pendingSettles.erase(settle); + continue; + } + VkBuffer source = import->second.buffer; + if (otherFamily) { + VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; + external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; + VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + aliasInfo.size = std::min(import->second.bytes, 65536); + aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + VkBuffer alias = VK_NULL_HANDLE; + if (createBuffer(context.device, &aliasInfo, nullptr, &alias) != VK_SUCCESS) { + state.pendingSettles.erase(settle); + continue; + } + if (bindBufferMemory(context.device, alias, import->second.memory, 0) != VK_SUCCESS) { + destroyBuffer(context.device, alias, nullptr); + state.pendingSettles.erase(settle); + continue; + } + batch.aliases.push_back(alias); + source = alias; + } + const VkBufferCopy region{0, (VkDeviceSize{index} * settleBatchCopies + batch.bases.size()) * 4u, 4}; + copy(batch.commands, source, deviceSettle.batchTarget, 1, ®ion); + settle->second = PendingSettle{SettlePhase::Submitted, index, context.device}; + batch.bases.push_back(base); + } + state.settleWanted.erase(state.settleWanted.begin(), state.settleWanted.begin() + static_cast(consumed)); + if ((result = context.Function("vkEndCommandBuffer")(batch.commands)) != VK_SUCCESS) return fail("vkEndCommandBuffer (settle batch)"); + if (batch.bases.empty()) return; + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &batch.commands; + if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); + batch.busy = true; + state.settlesSubmitted += batch.bases.size(); +} + +void reapSettleBatches(const Context& context, HostImports& state) { + const auto found = state.settles.find(context.device); + if (found == state.settles.end()) return; + auto& deviceSettle = found->second; + const auto status = context.Resolved(&DeviceFunctions::getFenceStatus, "vkGetFenceStatus"); + for (std::size_t index = 0; index < deviceSettle.batches.size(); ++index) { + auto& batch = deviceSettle.batches[index]; + if (!batch.busy) continue; + const auto result = status(context.device, batch.fence); + if (result == VK_NOT_READY) continue; + for (const auto base : batch.bases) { + const auto settle = state.pendingSettles.find(base); + if (settle == state.pendingSettles.end() || settle->second.phase != SettlePhase::Submitted || settle->second.batch != index || settle->second.device != context.device) continue; + if (result != VK_SUCCESS) { + state.pendingSettles.erase(settle); + continue; + } + settle->second.phase = SettlePhase::Settled; + state.settleReady.push_back(base); + } + for (const auto alias : batch.aliases) deviceSettle.settleDestroyBuffer(context.device, alias, nullptr); + batch.aliases.clear(); + batch.retired.clear(); + batch.bases.clear(); + batch.busy = false; + } +} + +void rewatchSettledImports(HostImports& state) { + if (state.settleReady.empty()) return; + std::vector> ranges; + std::vector deferred; + for (const auto base : state.settleReady) { + const auto settle = state.pendingSettles.find(base); + if (settle == state.pendingSettles.end() || settle->second.phase != SettlePhase::Settled) continue; + const auto import = state.imports.find(base); + if (import == state.imports.end() || !import->second.unwatched) { + state.pendingSettles.erase(settle); + continue; + } + const std::array, 1> span{{{base, base + import->second.bytes}}}; + if (StorageTexture::AnyPendingOverlaps(span)) { + deferred.push_back(base); + continue; + } + ranges.push_back(span.front()); + state.pendingSettles.erase(settle); + } + state.settleReady = std::move(deferred); + if (ranges.empty()) return; + GuestMemory::Rewatch(ranges); + for (const auto& [begin, end] : ranges) { + const auto import = state.imports.find(begin); + if (import == state.imports.end() || !GuestMemory::Watched(begin, static_cast(end - begin))) continue; + import->second.unwatched = false; + ++state.settlesRewatched; + } +} + +void settleUnwatchedImports(const Context& context, HostImports& state) { + if (state.pendingSettles.empty() || importsStale(context, state) || context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr || !GuestMemory::GpuMutex().HeldByThisThread()) return; + const auto now = std::chrono::steady_clock::now(); + if (now - state.settlePolled < settlePollPeriod) return; + state.settlePolled = now; + reapSettleBatches(context, state); + rewatchSettledImports(state); + submitSettleBatch(context, state); +} #endif void decideImportWatch(const Context& context, HostImports& state) { @@ -1000,8 +1359,8 @@ 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); // The bytes retireLeastRecent could free outside [begin, end). @@ -1013,7 +1372,33 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std: return bytes; } +struct ImportConstruction { + ImportConstruction(const Context& context, std::uint64_t base, std::uint64_t bytes) + : device(context.device), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0} {} + ImportConstruction(const ImportConstruction&) = delete; + ImportConstruction& operator=(const ImportConstruction&) = delete; + ~ImportConstruction() { + if (!armed) return; + if (entry.buffer != VK_NULL_HANDLE) destroyBuffer(device, entry.buffer, nullptr); + if (entry.memory != VK_NULL_HANDLE) freeMemory(device, entry.memory, nullptr); +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); +#endif + } + VkDevice device; + PFN_vkDestroyBuffer destroyBuffer; + PFN_vkFreeMemory freeMemory; + HostImport entry; + bool armed = true; +}; + +void ensurePinWaiter(); + const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { + ensurePinWaiter(); +#ifdef _WIN32 + settleUnwatchedImports(context, state); +#endif const bool fresh = !importsStale(context, state); if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; @@ -1032,8 +1417,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 +1440,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; @@ -1088,7 +1475,8 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import it = next; } if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget); - HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; + ImportConstruction owner(context, base, bytes); + auto& entry = owner.entry; entry.range = run.ranges.front(); entry.ranges = std::move(run.ranges); #ifdef _WIN32 @@ -1131,6 +1519,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,12 +1556,21 @@ 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) { -#ifdef _WIN32 - GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); -#endif failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast(base), static_cast(bytes), step, static_cast(result)); #ifdef _WIN32 @@ -1206,7 +1604,21 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); entry.joined = joined; entry.lastUse = ++state.useCounter; - return &state.imports.emplace(base, entry).first->second; +#ifdef _WIN32 + if (deferredSettles.load(std::memory_order_relaxed) && entry.alias == nullptr && state.watchPrivateImports && writable && base % settleBlockBytes == 0 && bytes % settleBlockBytes == 0) { + if (entry.unwatched) { + state.pendingSettles.insert_or_assign(base, PendingSettle{}); + state.settleWanted.push_back(base); + } else if (settled && !GuestMemory::Watched(base, static_cast(bytes))) { + entry.unwatched = true; + state.pendingSettles.insert_or_assign(base, PendingSettle{SettlePhase::Settled, 0}); + state.settleReady.push_back(base); + } + } +#endif + const auto inserted = state.imports.emplace(base, entry); + owner.armed = false; + return &inserted.first->second; } // The readable registered range containing [begin, end), or null. @@ -1221,6 +1633,9 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le // guest addresses. Walks the imports only when the registry changed since the last walk. void refreshImports(const Context& context, HostImports& state, const GuestAllocations::Lease& lease) { if (state.device != context.device) { +#ifdef _WIN32 + drainSettles(state); +#endif while (!state.imports.empty()) { auto node = state.imports.extract(state.imports.begin()); if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { @@ -1269,14 +1684,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) { @@ -1991,6 +2410,11 @@ void reportMirrors() { heapBytes = state.heapBytes; } AgcDriver::ProfilePrint_nid_no_patch("[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable, %zu heap %.1f MiB), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu sparse heaps bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs; heap refills %llu, unwatched heaps %llu, %llu heap mirrors released for imports (%.1f MiB)\n", count, bytes / 1048576.0, writable, heaps, heapBytes / 1048576.0, static_cast(state.builds), static_cast(state.subranges), static_cast(state.sparseBinds), static_cast(state.rebuilds), static_cast(state.refreshes), static_cast(state.blocksCompared), static_cast(state.blocksCopied), static_cast(state.syncs), static_cast(state.heapRefills), static_cast(state.heapUnwatched), static_cast(state.heapImported), state.heapImportedBytes / 1048576.0); +#ifdef _WIN32 + auto& imports = Imports(); + std::lock_guard importsLock(imports.mutex); + if (imports.settlesSubmitted != 0 || imports.settlesWaited != 0 || !imports.settles.empty()) AgcDriver::ProfilePrint_nid_no_patch("[buffers] host imports under %llu KiB: %llu settle reads submitted, %llu back in the write watch, %llu retired during their read (%llu unmaps waited for it), %zu pending; %llu larger imports settled inside their window\n", static_cast(settleFloorBytes >> 10u), static_cast(imports.settlesSubmitted), static_cast(imports.settlesRewatched), static_cast(imports.settlesParked), static_cast(imports.settleUnmapWaits), imports.settles.size(), static_cast(imports.settlesWaited)); +#endif } // Deferred lease release. The lease an address-based build takes (AcquireRegistered) is dropped by its @@ -2036,7 +2460,7 @@ SnapshotStats& Snapshots() { return stats; } -bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept { +bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { const auto start = std::chrono::steady_clock::now(); bool synced = false; bool drained = false; @@ -2056,19 +2480,85 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); return true; } + if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion"); if (GuestMemory::GpuMutex().HeldByThisThread()) { - try { - auto* recorder = Recorder::Active(); + auto* recorder = Recorder::Active(); + if (recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } + if (recorder != nullptr) recorder->FlushDeferredReleases(); + const auto end = static_cast(address) + bytes; + auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + { + std::lock_guard importsLock(imports.mutex); + for (auto it = imports.imports.begin(); it != imports.imports.end();) { + const auto base = it->first; + if (base >= end || address >= base + it->second.bytes) { + ++it; + continue; + } + const auto next = std::next(it); + retireImport(imports.device, imports, it, lease, false); + it = next; + retiredImport = true; + } + } +#ifdef _WIN32 + if (releaseSettleRetired(imports, address, end)) retiredImport = true; +#endif + if (retiredImport && recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } + if (retiredImport && recorder != nullptr) recorder->FlushDeferredReleases(); + } else { + std::lock_guard lock(GuestMemory::GpuMutex()); + auto& state = Leases(); + auto* recorder = Recorder::Active(); + const auto target = state.newestLeaseSerial; + if (recorder != nullptr && recorder == state.leaseRecorder && target > state.finishedLeaseSerial) { + Recorder::CountSync(4); + recorder->Submit(); + recorder->FinishUpTo(target); + state.finishedLeaseSerial = target; + synced = true; + } else if (recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } else { + std::this_thread::yield(); + } + if (recorder != nullptr) recorder->FlushDeferredReleases(); + + const auto end = static_cast(address) + bytes; + auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; + bool overlapsImport = false; + { + std::lock_guard importsLock(imports.mutex); + for (const auto& [base, entry] : imports.imports) { + if (base < end && address < base + entry.bytes) { + overlapsImport = true; + break; + } + } + } + if (overlapsImport) { if (recorder != nullptr && !recorder->Idle()) { Recorder::CountSync(4); recorder->Sync(); synced = true; drained = true; } - if (recorder != nullptr) recorder->FlushDeferredReleases(); - const auto end = static_cast(address) + bytes; - auto& imports = Imports(); - if (releaseParkedImports(imports, address, end)) retiredImport = true; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); { std::lock_guard importsLock(imports.mutex); @@ -2090,80 +2580,10 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept synced = true; drained = true; } - if (retiredImport && recorder != nullptr) recorder->FlushDeferredReleases(); - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); - } - } else { - try { - std::lock_guard lock(GuestMemory::GpuMutex()); - auto& state = Leases(); - auto* recorder = Recorder::Active(); - const auto target = state.newestLeaseSerial; - if (recorder != nullptr && recorder == state.leaseRecorder && target > state.finishedLeaseSerial) { - // The open batch may be the target; FinishUpTo waits front to back up to it only. - Recorder::CountSync(4); - recorder->Submit(); - recorder->FinishUpTo(target); - state.finishedLeaseSerial = target; - synced = true; - } else if (recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } else { - // Nothing recorded holds the lease: the holder is a lease-holding object that has - // yet to record (a test's, or a build whose worker gave up the lock); give it time. - std::this_thread::yield(); - } - if (recorder != nullptr) recorder->FlushDeferredReleases(); - - const auto end = static_cast(address) + bytes; - auto& imports = Imports(); - if (releaseParkedImports(imports, address, end)) retiredImport = true; - bool overlapsImport = false; - { - std::lock_guard importsLock(imports.mutex); - for (const auto& [base, entry] : imports.imports) { - if (base < end && address < base + entry.bytes) { - overlapsImport = true; - break; - } - } - } - if (overlapsImport) { - if (recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - { - std::lock_guard importsLock(imports.mutex); - for (auto it = imports.imports.begin(); it != imports.imports.end();) { - const auto base = it->first; - if (base >= end || address >= base + it->second.bytes) { - ++it; - continue; - } - const auto next = std::next(it); - retireImport(imports.device, imports, it, lease, false); - it = next; - retiredImport = true; - } - } - if (retiredImport && recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } - } - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); } +#ifdef _WIN32 + if (releaseSettleRetired(imports, address, end)) retiredImport = true; +#endif } auto& state = Leases(); std::lock_guard lock(state.mutex); @@ -2293,17 +2713,19 @@ void ClearHostImports(VkDevice device) { 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); - if (state.settleMemory != VK_NULL_HANDLE) state.settleFreeMemory(device, state.settleMemory, nullptr); - state.settleDevice = VK_NULL_HANDLE; - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; - state.settleCommands = VK_NULL_HANDLE; - state.settleFence = VK_NULL_HANDLE; - } + if (device == state.device) drainSettles(state); #endif + if (const auto found = state.settles.find(device); found != state.settles.end()) { +#ifdef _WIN32 + releaseSettleBatches(device, found->second); + std::erase_if(state.pendingSettles, [device](const auto& settle) { return settle.second.phase == SettlePhase::Submitted && settle.second.device == device; }); +#endif + const auto& settle = found->second; + if (settle.settleFence != VK_NULL_HANDLE) settle.destroyFence(device, settle.settleFence, nullptr); + if (settle.settleTarget != VK_NULL_HANDLE) settle.settleDestroyBuffer(device, settle.settleTarget, nullptr); + if (settle.settleMemory != VK_NULL_HANDLE) settle.settleFreeMemory(device, settle.settleMemory, nullptr); + state.settles.erase(found); + } if (state.device != device) return; for (const auto& [address, entry] : state.imports) { state.destroyBuffer(state.device, entry.buffer, nullptr); @@ -2549,17 +2971,42 @@ void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); } +void SetHostImportDeferredSettles(bool enabled) { +#ifdef _WIN32 + deferredSettles.store(enabled, std::memory_order_relaxed); +#else + static_cast(enabled); +#endif +} + std::uint64_t HostImportLiveBytes() { auto& state = Imports(); std::lock_guard lock(state.mutex); return state.liveBytes; } +HostImportSettleCounts HostImportSettles() { +#ifdef _WIN32 + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched, state.settlesParked, state.settleUnmapWaits}; +#else + return {}; +#endif +} + +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(); auto& state = Imports(); std::lock_guard lock(state.mutex); +#ifdef _WIN32 + settleUnwatchedImports(context, state); +#endif // A hit is only valid while the registry has not changed since the imports were reconciled. if (!importsStale(context, state)) { if (const auto* entry = findImport(state, address, address + bytes)) { @@ -2572,6 +3019,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 +3086,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 +3401,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 +3423,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; } @@ -3497,7 +3962,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { // device-local buffer that the CPU fallback replaces (none without a recorder to // record the copies: UploadFinish then binds the region in place). bool staged = Recorder::Active() != nullptr && stagingEligible(region, addressable); - const bool misaligned = !bindableInPlace(region.begin - entry->base, addressable); + const bool misaligned = !bindableInPlace(HostImportOffset(*entry, region.begin), addressable); if (staged) { const auto allocateStart = std::chrono::steady_clock::now(); region.buffer = stagingBuffer(context, static_cast(region.end - region.begin), gpuCopyUsage(addressable)); @@ -3684,7 +4149,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; @@ -3701,7 +4166,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { // A staged region (see stagingEligible) is copied out of the import even when aligned; // without a recorder to record the copies it binds in place like any other. const bool staged = recorder != nullptr && stagingEligible(region, addressable); - if (entry != nullptr && !staged && bindableInPlace(region.begin - entry->base, addressable)) { + if (entry != nullptr && !staged && bindableInPlace(HostImportOffset(*entry, region.begin), addressable)) { region.direct = entry; region.runCut = runImport; region.snapshot.clear(); @@ -3717,7 +4182,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { copyOnGpu = true; region.deviceLocal = staged; region.copySource = entry->buffer; - region.copySourceBase = entry->base; + region.copySourceBase = entry->base - entry->lead; } } if (region.direct != nullptr) { @@ -3792,7 +4257,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 +4501,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) { @@ -4045,7 +4510,7 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: return std::string(text); }; if (!(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr))) Require(false, describe("exceeds")); - const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin; + const auto base = region.direct != nullptr ? region.direct->base - region.direct->lead : region.mirror != nullptr ? region.mirror->base : region.begin; const auto offset = address - base; Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment"); Require(base % 4 == 0, "a guest buffer view in a GPU owner that does not start at a DWORD boundary is not implemented"); @@ -4064,7 +4529,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 +4548,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 +4641,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 +4701,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 +4721,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 +4734,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 +4801,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 +4818,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..4ca7a22552 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; @@ -465,6 +676,25 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar return acquire(context, target).view; } +namespace { + +bool redBlueSwapped(VkFormat rendered, VkFormat sampled) { + const auto pair = [&](VkFormat a, VkFormat b) { return (rendered == a && sampled == b) || (rendered == b && sampled == a); }; + return pair(VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM) || pair(VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB) || pair(VK_FORMAT_B8G8R8A8_SNORM, VK_FORMAT_R8G8B8A8_SNORM) || pair(VK_FORMAT_B8G8R8A8_UINT, VK_FORMAT_R8G8B8A8_UINT) || pair(VK_FORMAT_B8G8R8A8_SINT, VK_FORMAT_R8G8B8A8_SINT); +} + +VkComponentMapping swapRedBlue(VkComponentMapping components) { + const auto swapped = [](VkComponentSwizzle swizzle, VkComponentSwizzle identity) { + if (swizzle == VK_COMPONENT_SWIZZLE_IDENTITY) swizzle = identity; + if (swizzle == VK_COMPONENT_SWIZZLE_R) return VK_COMPONENT_SWIZZLE_B; + if (swizzle == VK_COMPONENT_SWIZZLE_B) return VK_COMPONENT_SWIZZLE_R; + return swizzle; + }; + return {swapped(components.r, VK_COMPONENT_SWIZZLE_R), swapped(components.g, VK_COMPONENT_SWIZZLE_G), swapped(components.b, VK_COMPONENT_SWIZZLE_B), swapped(components.a, VK_COMPONENT_SWIZZLE_A)}; +} + +} + std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(targetsMutex()); const auto format = ResolveTextureFormat(resource.format); @@ -473,12 +703,14 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const 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->Sampled(components); + const bool sameShape = rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0; + if (sameShape && rendered.format == format) return target->Sampled(components); + if (sameShape && redBlueSwapped(rendered.format, format)) return target->Sampled(swapRedBlue(components)); other = target.get(); } 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 +722,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..e718cee2f4 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 @@ -94,6 +96,10 @@ void ValidateDepthBounds(const Context& context, const State& state) { Require(!state.depthBoundsTest || context.depthRangeUnrestricted || (state.minDepthBounds >= 0.0f && state.minDepthBounds <= 1.0f && state.maxDepthBounds >= 0.0f && state.maxDepthBounds <= 1.0f), "depth bounds outside [0, 1] require VK_EXT_depth_range_unrestricted"); } +bool BarycentricPatches(const State& state, std::span shaders) { + return !state.rectList && state.stages.path == ShaderPath::Vertex && std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::TessellationControl; }); +} + void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders) { Require(state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT || state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, "invalid provoking vertex mode"); if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) { @@ -104,8 +110,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(!BarycentricPatches(state, shaders) && 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 +124,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; } @@ -190,13 +196,15 @@ void appendKey(std::vector& key, const TValue& value) { bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; - if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || 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; @@ -306,6 +314,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); + const bool barycentricPatches = BarycentricPatches(state, shaders); + if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart"); if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders"); if (state.stages.tessellation) { Require(context.tessellationShader, "device does not support tessellation shaders"); @@ -421,7 +431,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput input.vertexAttributeDescriptionCount = static_cast(vertexInput.attributes.size()); input.pVertexAttributeDescriptions = vertexInput.attributes.data(); VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; - assembly.topology = state.topology; + assembly.topology = BarycentricPatches(state, shaders) ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : state.topology; const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; assembly.primitiveRestartEnable = state.primitiveRestart && (!listTopology || context.primitiveListRestart) ? VK_TRUE : VK_FALSE; // Viewport and scissor are set per draw (Begin), so they do not multiply pipelines; the depth @@ -508,8 +518,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.stageCount = static_cast(stages.size()); pipelineInfo.pStages = stages.data(); VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO}; - if (state.rectList || state.stages.tessellation) { - tessellation.patchControlPoints = state.rectList ? 3u : state.stages.tessellation->inputControlPoints; + if (state.rectList || state.stages.tessellation || barycentricPatches) { + tessellation.patchControlPoints = state.stages.tessellation ? state.stages.tessellation->inputControlPoints : 3u; pipelineInfo.pTessellationState = &tessellation; } pipelineInfo.pVertexInputState = state.stages.mesh ? nullptr : &input; @@ -711,6 +721,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 +790,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 +853,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 +890,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 +917,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 +930,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 +943,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 +960,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 +982,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 dced3d585e..7ba94904a1 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; @@ -527,7 +549,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; } @@ -544,11 +566,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; @@ -590,10 +618,11 @@ std::shared_ptr cachedTexture(const Context& context, std::span= 4 ? ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) : 0u; - if (depthBitsWidth == 32u) { - char text[160]; - std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast(resource.baseAddress)); - throw std::runtime_error(text); + constexpr auto float32 = static_cast(ShaderRecompiler::IrBufferFormat::Format32Float); + if (depthBitsWidth == 32u && resource.format != float32) { + auto bits = resource; + bits.format = float32; + return cachedTexture(context, words, bits, components, guestBytes, depthCompare); } constexpr auto unorm16 = static_cast(ShaderRecompiler::IrBufferFormat::Format16UNorm); if (depthBitsWidth == 16u && resource.format != unorm16) { @@ -724,6 +753,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spansource != nullptr) { @@ -965,6 +995,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); @@ -1007,6 +1038,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; } @@ -1017,10 +1053,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) { @@ -1037,6 +1077,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; { @@ -1116,6 +1160,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); @@ -1167,6 +1295,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"); } @@ -1504,6 +1633,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; @@ -1519,10 +1652,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) { @@ -1587,10 +1721,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; @@ -1685,6 +1822,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; @@ -1699,7 +1837,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; @@ -1800,7 +1944,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; @@ -1850,6 +1995,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) { @@ -1863,6 +2009,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); @@ -2001,6 +2157,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"); @@ -2073,6 +2230,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}; } @@ -2115,6 +2273,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) { @@ -2585,6 +2747,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 @@ -2688,6 +2854,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; @@ -3146,13 +3316,65 @@ std::shared_ptr ResourceCache::Find(const Key& key) { std::lock_guard lock(mutex); const auto found = index.find(key); if (found == index.end()) { + noteReturn(key); noteMiss(key); return nullptr; } entries.splice(entries.begin(), entries, found->second); + noteUse(key); return found->second->second; } +void ResourceCache::noteEvicted(const Key& key) { + if (fixedCapacity) return; + const auto hash = KeyHash{}(key); + if (const auto found = evictedKeys.find(hash); found != evictedKeys.end()) evictedOrder.erase(found->second); + evictedOrder.push_front(hash); + evictedKeys[hash] = evictedOrder.begin(); + while (evictedOrder.size() > capacity) { + evictedKeys.erase(evictedOrder.back()); + evictedOrder.pop_back(); + } + if (++evictionsSinceCheck < capacity / 4u) return; + const bool churning = returnsSinceCheck * 2u > evictionsSinceCheck; + evictionsSinceCheck = 0; + returnsSinceCheck = 0; + if (!churning || capacity >= MaximumEntries) return; + const auto previous = capacity; + capacity = std::min(capacity * 2u, MaximumEntries); + if (shrankLast) shrinkWindow = std::min(shrinkWindow * 2u, MaximumShrinkWindow); + shrankLast = false; + usedKeys.clear(); + usesSinceCheck = 0; + std::fprintf(stderr, "[rescache] capacity %zu -> %zu entries: keys it evicted keep coming back\n", previous, capacity); +} + +void ResourceCache::noteUse(const Key& key) { + if (fixedCapacity || capacity <= baseEntries) return; + usedKeys.insert(KeyHash{}(key)); + if (++usesSinceCheck < capacity * shrinkWindow) return; + const auto used = usedKeys.size(); + const auto uses = usesSinceCheck; + usedKeys.clear(); + usesSinceCheck = 0; + if (used * 4u > capacity) return; + const auto previous = capacity; + capacity = std::max(capacity / 2u, baseEntries); + shrankLast = true; + evictionsSinceCheck = 0; + returnsSinceCheck = 0; + std::fprintf(stderr, "[rescache] capacity %zu -> %zu entries: %zu keys served the last %zu uses\n", previous, capacity, used, uses); +} + +void ResourceCache::noteReturn(const Key& key) { + if (fixedCapacity) return; + const auto found = evictedKeys.find(KeyHash{}(key)); + if (found == evictedKeys.end()) return; + evictedOrder.erase(found->second); + evictedKeys.erase(found); + ++returnsSinceCheck; +} + namespace { void traceMiss(ChurnProfile& churn, bool draw, std::uint64_t variants, const ContentKeyDiff* parts) { @@ -3281,12 +3503,15 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr reso } entries.emplace_front(key, std::move(resources)); index.emplace(key, entries.begin()); - static const std::size_t capacity = [] { + if (capacity == 0) { const char* value = std::getenv("APS5_RESOURCE_CACHE_ENTRIES"); - const auto parsed = value ? std::strtoull(value, nullptr, 10) : 1024ull; - return static_cast(parsed != 0 ? parsed : 1024ull); - }(); + const auto parsed = value ? std::strtoull(value, nullptr, 10) : 0ull; + fixedCapacity = parsed != 0; + capacity = fixedCapacity ? static_cast(parsed) : baseEntries; + } + noteUse(key); while (entries.size() > capacity) { + noteEvicted(entries.back().first); if (evicted != nullptr) evicted->push_back(std::move(entries.back().second)); index.erase(entries.back().first); entries.pop_back(); @@ -3348,6 +3573,7 @@ bool ResourceCache::Touch(const Key& key) { const auto found = index.find(key); if (found == index.end()) return false; entries.splice(entries.begin(), entries, found->second); + noteUse(key); return true; } @@ -3363,6 +3589,21 @@ void ResourceCache::Clear() { std::lock_guard lock(mutex); index.clear(); entries.clear(); + evictedKeys.clear(); + evictedOrder.clear(); + evictionsSinceCheck = 0; + returnsSinceCheck = 0; + usedKeys.clear(); + usesSinceCheck = 0; + shrinkWindow = baseShrinkWindow; + shrankLast = false; + if (!fixedCapacity) capacity = 0; +} + +void ResourceCache::SetBase(std::size_t entries, std::size_t window) { + std::lock_guard lock(mutex); + baseEntries = entries; + baseShrinkWindow = window; } std::size_t ResourceCache::Size() const { @@ -3370,6 +3611,11 @@ std::size_t ResourceCache::Size() const { return entries.size(); } +std::size_t ResourceCache::Capacity() const { + std::lock_guard lock(mutex); + return capacity == 0 ? baseEntries : capacity; +} + std::uint64_t ResourceCache::TableElementBudget() { static const std::uint64_t elements = [] { const char* value = std::getenv("APS5_RESOURCE_CACHE_TABLE_ELEMENTS"); @@ -3398,7 +3644,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); @@ -3425,7 +3671,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; } @@ -3496,9 +3747,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}); @@ -3551,6 +3804,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; @@ -3561,29 +3815,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; @@ -3802,7 +4069,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) { @@ -4072,6 +4339,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; @@ -4093,6 +4375,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; @@ -4188,14 +4474,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); @@ -4224,11 +4512,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); @@ -4272,6 +4561,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; @@ -4358,15 +4686,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 @@ -4382,18 +4715,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; } @@ -4402,14 +4739,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&) { } } @@ -4515,13 +4856,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; @@ -4561,7 +4960,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; } @@ -4632,7 +5031,8 @@ std::shared_ptr ShaderResources::PrepareDrawBindi for (std::size_t index = 0; index < selected.size(); ++index) snapshotIndices[selected[index]] = index; auto nextWrite = writes.begin(); for (const auto& binding : bindings) { - while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding) { + const auto original = setOf(binding) == 0 ? _set : fragment.set; + while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding && nextWrite->dstSet == original) { auto& write = *nextWrite++; write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set; if (binding.allocations.empty()) continue; @@ -4670,7 +5070,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])); } @@ -4689,7 +5089,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..39ddfd7f61 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; @@ -115,7 +115,7 @@ struct Module { return; } // One gl_ClipDistance array holds the guest clip distances and the recompiler's all-zero position distance. - if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) { + if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) { const auto signature = Signature(type); auto& count = value == spv::BuiltInClipDistance ? clipDistances : cullDistances; Require(count == 0 && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']', "invalid or duplicate clip or cull distance output"); @@ -131,11 +131,13 @@ struct Module { return; } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation) { - 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"); + const bool clipInput = value == spv::BuiltInClipDistance && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']'; + Require(input && (clipInput || (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 +153,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 +187,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"); @@ -250,7 +253,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityStencilExportEXT; - if ((vertex || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { + if ((vertex || control || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { char word[16]; std::snprintf(word, sizeof(word), "0x%08x", state.paClVsOutCntl); Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : (vertex || mesh) && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); @@ -262,7 +265,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || - ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + ((vertex || control || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (vertex && capability == spv::CapabilityDrawParameters) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || @@ -331,9 +334,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: @@ -429,6 +442,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto it = module.modes.find(name); Require(it != module.modes.end() && it->second == operands, "missing or incompatible shader execution mode"); }; + const bool barycentricPatches = (control || evaluation) && !state.rectList && !state.stages.tessellation && state.stages.path == ShaderPath::Vertex; if (mesh) { Require(state.stages.mesh.has_value(), "mesh configuration is missing"); const auto& config = *state.stages.mesh; @@ -437,8 +451,14 @@ 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 (barycentricPatches) { + if (control) mode(spv::ExecutionModeOutputVertices, {3u}); + mode(spv::ExecutionModeTriangles, {}); + mode(spv::ExecutionModeSpacingEqual, {}); + mode(spv::ExecutionModeVertexOrderCw, {}); + Require(module.modes.size() == (control ? 4u : 3u), "unsupported generated tessellation execution mode"); } else if (control) { Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints}); @@ -486,12 +506,12 @@ 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"); } else { - const auto expected = state.rectList ? (control && input ? 3u : 4u) : control && input ? state.stages.tessellation->inputControlPoints : state.stages.tessellation->outputControlPoints; + const auto expected = barycentricPatches ? 3u : state.rectList ? (control && input ? 3u : 4u) : control && input ? state.stages.tessellation->inputControlPoints : state.stages.tessellation->outputControlPoints; Require(length->second == expected, "tessellation interface array disagrees with control-point count"); } typeId = outer[2]; @@ -537,10 +557,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; } @@ -592,9 +612,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage"); } Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); - Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V"); + Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); - if (mesh) Require(module.primitiveIndices, "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 +622,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 +635,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; @@ -623,7 +643,9 @@ std::set ValidateShaders(std::span shaders, Require(state.stages.mesh.has_value() == mesh && state.stages.tessellation.has_value() == tessellation, "graphics stage configuration disagrees with its path"); Require(!state.rectList || (state.stages.path == ShaderPath::Vertex && state.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST && state.cullMode == VK_CULL_MODE_NONE), "invalid rect-list pipeline state"); const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u; - Require(geometry || shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count"); + const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active; + Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count"); + if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list"); if (geometry) { Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives"); @@ -632,11 +654,11 @@ std::set ValidateShaders(std::span shaders, static_cast(AssemblePushConstants(shaders)); Module previous; for (std::size_t i = 0; i < shaders.size(); ++i) { - const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; + const auto expected = state.rectList || patches ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation); + 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); @@ -663,7 +685,7 @@ std::set ValidateShaders(std::span shaders, const auto color = std::find_if(state.colors.begin(), state.colors.end(), [&](const ColorTarget& target) { return target.exportIndex == location; }); const bool uintExport = color != state.colors.end() && color->uintExport; const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None; - Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed 10_11_11 unorm attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); + Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); locations.insert(location); } Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c16eb7847a..66c444520c 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); @@ -701,6 +743,10 @@ struct DecodedColorFormat { ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None; }; +const char* PackedColorTargetName(ShaderRecompiler::ColorExportPacking packing) { + return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm"; +} + // CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list // components from the least significant bits, as Vulkan's non-packed formats do. DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { @@ -711,7 +757,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 != 7 && format != 10 && format != 12 && format != 16) return fail(); switch (format) { case 1: if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); @@ -749,6 +795,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (swap != 0 || (number != floating && number != unorm)) return fail(); if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11}; return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4}; + case 7: + if (swap != 2 || number != floating) return fail(); + return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10}; case 9: // COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing. if (number != unorm) return fail(); @@ -779,6 +828,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 +928,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 +941,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 +967,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 +1056,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 +1086,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 +1127,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 +1178,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()) { @@ -1239,16 +1313,22 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } } if (color.packing != ShaderRecompiler::ColorExportPacking::None) { + const std::string packed = std::string(color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "an " : "a ") + PackedColorTargetName(color.packing); const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; - if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into a 10_11_11 unorm target is unsupported"); - Require(!state.blendEnable, "blending into a 10_11_11 unorm color target is unsupported"); - Require((exportedMask & 7u) == 7u, "partial writes of a 10_11_11 unorm color target are unsupported"); + if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); + Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); + Require((exportedMask & 7u) == 7u, "partial writes of " + packed + " color target are unsupported"); } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; 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 +1400,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 +1444,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 +1486,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 + // ROUND_MODE (bit 18) only affects unorm rounding and packed float truncation. 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 +1505,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; @@ -1443,13 +1528,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.surfaceExtent = color.extent; color.mipCount = maxMip + 1u; color.mip = viewMip; + std::uint32_t firstTailLevel = maxMip + 1u; if (maxMip != 0) { - // A mipmapped surface is addressed like a texture; the view renders into one mip of it. const auto mips = ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, maxMip + 1u); const auto& mip = mips.at(viewMip); mipOffset = mip.tiledOffset; color.mipTail = mip.tail; color.extent = {mip.width, mip.height}; + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (mips[level].tail) { + firstTailLevel = level; + break; + } + } } const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); const auto high = read(cx, 0x390 + slot); @@ -1478,23 +1569,44 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.format = decoded.format; color.componentMapping = decoded.componentMapping; color.packing = decoded.packing; - Require(color.packing == ShaderRecompiler::ColorExportPacking::None || (info & 0x10000000u) == 0, "DCC-compressed 10_11_11 unorm color targets are unsupported"); - Require(color.packing == ShaderRecompiler::ColorExportPacking::None || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); + if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); + Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); + Require(color.packing != ShaderRecompiler::ColorExportPacking::Float11_11_10 || (info & 0x00040000u) != 0, "rounding (ROUND_MODE 0) 11_11_10 float color targets are unsupported"); for (std::uint32_t word = 0; word < 2; ++word) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) { - Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + Require(color.samples > 1 || (!volume && slice == 0 && color.arrayLastSlice == 0), "CMASK fast clears of a 3D or array color target are unsupported"); Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)"); - if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (color.samples == 1) { + Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (maxMip != 0) Require(color.elementBytes > 2, "CMASK fast clears of mipmapped color targets with 8 or 16 bpp are unsupported"); + } const auto cmaskHigh = find(cx, 0x398 + slot); const auto cmask = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); Require((info & 0x2000u) == 0 || cmask != 0, "CMASK fast clears without a CMASK address are unsupported"); if (cmask != 0) { - color.cmaskAddress = cmask; + std::size_t precedingBlocks = 0; + std::size_t totalBlocks = static_cast((color.surfaceExtent.width + 1023u) / 1024u) * ((color.surfaceExtent.height + 511u) / 512u); + if (maxMip != 0) { + if (firstTailLevel > 0) { + precedingBlocks = firstTailLevel < color.mipCount ? 1u : 0u; + for (auto level = firstTailLevel; level-- > 1u;) { + const auto w = std::max((color.surfaceExtent.width + (1u << level) - 1u) >> level, 1u); + const auto h = std::max((color.surfaceExtent.height + (1u << level) - 1u) >> level, 1u); + precedingBlocks += static_cast((w + 1023u) / 1024u) * ((h + 511u) / 512u); + } + totalBlocks += precedingBlocks; + } else { + totalBlocks = 1u; + } + } + const auto cmaskMip0Offset = precedingBlocks * CmaskLayout::Alignment; + const auto totalCmaskBytes = totalBlocks * CmaskLayout::Alignment; + color.cmaskAddress = cmask + cmaskMip0Offset; color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); - if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(cmask), totalCmaskBytes, CmaskLayout::Alignment, true); } color.cmaskFastClear = (info & 0x2000u) != 0; } @@ -1504,6 +1616,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) { @@ -1573,6 +1690,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); + if (source.packing != ShaderRecompiler::ColorExportPacking::None) throw std::runtime_error("AGC graphics: CB resolves of " + std::string(PackedColorTargetName(source.packing)) + " color targets are unsupported"); requireCovered(destination); pass.source = source; pass.targets.push_back(destination); @@ -1624,11 +1742,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..f6b231a262 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(); @@ -2307,7 +2319,7 @@ void StorageTexture::upload(const std::vector* layers) { batch = std::make_unique(context); commands = batch->Handle(); } - const auto importOffset = descriptor.baseAddress - import->base; + const auto importOffset = HostImportOffset(*import, descriptor.baseAddress); if (direct) { recordDirectUploadBarrier(commands, layers == nullptr); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { @@ -2733,7 +2745,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< batch = std::make_unique(context); commands = batch->Handle(); } - const auto importOffset = descriptor.baseAddress - import.base; + const auto importOffset = HostImportOffset(import, descriptor.baseAddress); if (direct) { recordDirectUploadBarrier(commands, discard); for (const auto& window : windows) { @@ -2866,7 +2878,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se if (!shadowed.empty() && shadowed.back().end == guestBegin && shadowed.back().slab == destination->slab) shadowed.back().end = pieceEnd; else shadowed.push_back({guestBegin, pieceEnd, destination->slab}); } else { - importCopies[segment].push_back({scratchOffset, guestBegin - import.base, pieceEnd - guestBegin}); + importCopies[segment].push_back({scratchOffset, HostImportOffset(import, guestBegin), pieceEnd - guestBegin}); if (!imported.empty() && imported.back().second == guestBegin) imported.back().second = pieceEnd; else imported.emplace_back(guestBegin, pieceEnd); } @@ -2961,7 +2973,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se } for (const auto& [begin, end, slab, import] : seeds) { // The piece's own slab (a seeded unit is one the piece covers partly). - const VkBufferCopy seed{begin - import->base, SlabOffset(*import, *slab, begin), end - begin}; + const VkBufferCopy seed{HostImportOffset(*import, begin), SlabOffset(*import, *slab, begin), end - begin}; context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, import->buffer, slab->buffer, 1, &seed); NoteShadowSeed(begin, end); } @@ -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(); @@ -4774,7 +4792,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (const auto& [from, to] : keep) { const auto begin = std::max(from, segment.begin); const auto end = std::min(to, segment.end); - if (begin < end) copies.push_back({begin - descriptor.baseAddress, begin - segment.import->base, end - begin}); + if (begin < end) copies.push_back({begin - descriptor.baseAddress, HostImportOffset(*segment.import, begin), end - begin}); } if (!copies.empty()) context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), segment.import->buffer, static_cast(copies.size()), copies.data()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 9397681655..62bd3365ef 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -144,8 +144,8 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem // per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements. std::array values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u}; if (thick) { - const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u; - Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X"); + const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kS4KBX ? 0x115u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u; + Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X"); const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes); Require(equation != nullptr, "no thick swizzle equation for the element size"); values[2] = 2u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 6eca2ae659..a2c51f1e0c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -262,11 +262,12 @@ std::array ThickBlockExtent(TextureTileMode tileMode, std::uin Require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "unsupported bytes per element for thick texture geometry"); const auto index = static_cast(std::countr_zero(bytesPerElement)); switch (tileMode) { - case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; + case TextureTileMode::kStandard4KB: + case TextureTileMode::kS4KBX: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; case TextureTileMode::kStandard64KB: case TextureTileMode::kS64KBX: case TextureTileMode::kD64KBX: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]}; - default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_64KB_S, SW_64KB_S_X and SW_64KB_D_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); + default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X and SW_64KB_D_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); } } @@ -316,7 +317,7 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s auto firstTailLevel = mipCount; if (tileMode != TextureTileMode::kLinear) { block = ThickBlockExtent(tileMode, bytesPerElement); - blockBytes = tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u; + blockBytes = tileMode == TextureTileMode::kStandard4KB || tileMode == TextureTileMode::kS4KBX ? 4096u : 65536u; const auto log2BlockBytes = static_cast(std::countr_zero(blockBytes)); const auto tailWidth = log2BlockBytes % 3u == 1u ? block[0] >> 1u : block[0]; const auto tailHeight = log2BlockBytes % 3u == 0u ? block[1] >> 1u : block[1]; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index 45d57d64da..cd2998c06b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -67,11 +67,12 @@ Statistics& Stats() { struct UnitShadow { Context context; - // The import's range and buffer (taken at creation: a publish under the import table's mutex - // must not look them up), and the unit base (the import's base rounded down to a unit). + // The import's range, buffer and lead (taken at creation: a publish under the import table's + // mutex must not look them up), and the unit base (the import's base rounded down to a unit). std::uint64_t importBase = 0; std::uint64_t importBytes = 0; VkBuffer importBuffer = VK_NULL_HANDLE; + std::uint64_t importLead = 0; std::uint64_t base = 0; // Per unit: 0 = never shadowed, else the tracker generation the slab bytes are current at; // whether a slab -> import copy was recorded at that generation. @@ -162,6 +163,7 @@ std::shared_ptr findOrCreate(Shadows& registry, const Context& conte shadow->importBase = import.base; shadow->importBytes = import.bytes; shadow->importBuffer = import.buffer; + shadow->importLead = import.lead; shadow->base = import.base & ~(UnitBytes - 1); const auto units = (import.base + import.bytes - shadow->base + UnitBytes - 1) / UnitBytes; shadow->generation.assign(static_cast(units), 0); @@ -279,7 +281,7 @@ void verifyPublished(const UnitShadow& shadow, const std::vector& co RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); batch.SubmitAndWait(); host.Invalidate(); - const auto address = shadow.importBase + region.dstOffset; + const auto address = shadow.importBase + region.dstOffset - shadow.importLead; Stats().verified.fetch_add(1, std::memory_order_relaxed); if (!GuestMemory::Accessible(reinterpret_cast(address), static_cast(region.size)) || std::memcmp(reinterpret_cast(address), host.Bytes().data(), static_cast(region.size)) != 0) { Stats().mismatches.fetch_add(1, std::memory_order_relaxed); @@ -336,7 +338,7 @@ std::size_t publishUnits(const std::shared_ptr& shadow, std::uint64_ } const auto [begin, end] = shadow->UnitRange(unit); if (end <= begin) continue; - const VkBufferCopy region{(unit - slab->firstUnit) * UnitBytes + (begin - shadow->UnitBegin(unit)), begin - shadow->importBase, end - begin}; + const VkBufferCopy region{(unit - slab->firstUnit) * UnitBytes + (begin - shadow->UnitBegin(unit)), begin - shadow->importBase + shadow->importLead, end - begin}; if (!copies.empty() && copies.back().slab == slab && copies.back().regions.back().dstOffset + copies.back().regions.back().size == region.dstOffset) { copies.back().regions.back().size += region.size; ranges.back().second = end; @@ -606,7 +608,7 @@ std::vector ShadowSources(const Context& context, const HostImport& i std::vector result; if (runs.empty()) return result; const auto importPiece = [&](std::uint64_t begin, std::uint64_t end) { - result.push_back({begin, end, import.buffer, surfaceBase + begin - import.base, false, nullptr}); + result.push_back({begin, end, import.buffer, HostImportOffset(import, surfaceBase + begin), false, nullptr}); if (countReads) Stats().detiledImportBytes.fetch_add(end - begin, std::memory_order_relaxed); }; std::shared_ptr shadow; @@ -668,7 +670,7 @@ std::vector ShadowSources(const Context& context, const HostImport& i slab->lastUse = present; result.push_back({pieceBegin, pieceEnd, slab->buffer, SlabOffset(import, *slab, surfaceBase + pieceBegin), true, slab}); } else { - result.push_back({pieceBegin, pieceEnd, import.buffer, surfaceBase + pieceBegin - import.base, false, nullptr}); + result.push_back({pieceBegin, pieceEnd, import.buffer, HostImportOffset(import, surfaceBase + pieceBegin), false, nullptr}); } (isFresh ? shadowBytes : importBytes) += pieceEnd - pieceBegin; } 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/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 860643bd27..61a1612f87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -102,6 +102,7 @@ public: context.bufferDeviceAddress = true; context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { 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/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp index 1a811755b3..2cc4a8b370 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -2,6 +2,8 @@ #include "Optimization/ConstantFolder.hpp" #include "Optimization/DeadCodeEliminator.hpp" #include +#include +#include #include using namespace ShaderRecompiler; @@ -77,11 +79,54 @@ void CheckDiamond(unsigned mode) { } } +void CheckLongAncillaryForwarding() { + IrProgram program; + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = 0u; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + program.SetEntryBlock(block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::PackedAncillary)), &builder.Constant(0u)}); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& condition = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(16u)}); + auto& zero = builder.Constant(0u); + IrValue* forwarded = &ancillary; + for (std::uint32_t link = 0; link < 20000u; ++link) forwarded = &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &zero, forwarded}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {forwarded, &builder.Constant(8u), &builder.Constant(4u)}); + builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field}); + struct Run { + IrProgram& program; + std::string error; + } run{program, {}}; + pthread_attr_t attributes; + pthread_attr_init(&attributes); + pthread_attr_setstacksize(&attributes, 256u * 1024u); + pthread_t thread; + const auto body = +[](void* argument) -> void* { + auto& state = *static_cast(argument); + try { + ConstantFolder{}.Fold(state.program); + } catch (const std::exception& error) { + state.error = error.what(); + } + return nullptr; + }; + Require(pthread_create(&thread, &attributes, body, &run) == 0, "long ancillary forwarding: cannot start the folding thread"); + pthread_join(thread, nullptr); + pthread_attr_destroy(&attributes); + Require(run.error.empty(), "long ancillary forwarding: folding failed"); +} + } int main() { try { for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode); + CheckLongAncillaryForwarding(); std::puts("constant phi dominance tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp new file mode 100644 index 0000000000..883659b077 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp @@ -0,0 +1,121 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Program { + IrProgram program; + IrBuilder builder{program}; + IrValue* lane = nullptr; + IrValue* condition = nullptr; + IrValue* other = nullptr; + std::uint32_t values = 0; + + Program() { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = 0; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + program.SetEntryBlock(block); + builder.SetInsertionPoint(block); + lane = &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + condition = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(16u)}); + other = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(8u)}); + } + + IrValue& value() { + return builder.Emit(IrOpcode::IAdd32, IrType::U32, {lane, &builder.Constant(++values)}); + } + + IrValue& select(IrValue& selector, IrValue& onTrue, IrValue& onFalse) { + return builder.Emit(IrOpcode::SelectU32, IrType::U32, {&selector, &onTrue, &onFalse}); + } + + const IrValue* folded(IrValue& result) { + auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&result}); + ValidateProgram(program, true); + ConstantFolder{}.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + ValidateProgram(program, true); + return reference.Argument(0)->Resolve(); + } +}; + +bool Selects(const IrValue* value, const IrValue* selector, const IrValue* onTrue, const IrValue* onFalse) { + return value->Opcode() == IrOpcode::SelectU32 && value->Argument(0)->Resolve() == selector && value->Argument(1)->Resolve() == onTrue && value->Argument(2)->Resolve() == onFalse; +} + +void CheckFalseArm() { + Program p; + auto& kept = p.value(); + auto& dropped = p.value(); + auto& fallback = p.value(); + auto& inner = p.select(*p.condition, dropped, fallback); + const auto* result = p.folded(p.select(*p.condition, kept, inner)); + Require(Selects(result, p.condition, &kept, &fallback), "a select in the false arm of a select on the same condition was not folded to its false arm"); + Require(!inner.HasUses(), "the folded inner select is still used"); +} + +void CheckTrueArm() { + Program p; + auto& kept = p.value(); + auto& dropped = p.value(); + auto& fallback = p.value(); + const auto* result = p.folded(p.select(*p.condition, p.select(*p.condition, kept, dropped), fallback)); + Require(Selects(result, p.condition, &kept, &fallback), "a select in the true arm of a select on the same condition was not folded to its true arm"); +} + +void CheckChain() { + Program p; + auto& last = p.value(); + auto& fallback = p.value(); + auto* chain = &p.select(*p.condition, p.value(), fallback); + chain = &p.select(*p.condition, p.value(), *chain); + const auto* result = p.folded(p.select(*p.condition, last, *chain)); + Require(Selects(result, p.condition, &last, &fallback), "a chain of selects on one condition did not fold to the last write and the fallback"); +} + +void CheckEqualArms() { + Program p; + auto& same = p.value(); + const auto* result = p.folded(p.select(*p.condition, same, p.select(*p.condition, p.value(), same))); + Require(result == &same, "a select whose arms become equal after folding was not replaced by that value"); +} + +void CheckOtherCondition() { + Program p; + auto& kept = p.value(); + auto& inner = p.select(*p.other, p.value(), p.value()); + const auto* result = p.folded(p.select(*p.condition, kept, inner)); + Require(Selects(result, p.condition, &kept, &inner), "a select nested under a different condition was folded"); +} + +} + +int main() { + try { + CheckFalseArm(); + CheckTrueArm(); + CheckChain(); + CheckEqualArms(); + CheckOtherCondition(); + std::puts("constant select tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index 3db6a8ce08..cc952f5ea9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -1,11 +1,20 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/DenormalFlushEliminator.hpp" +#include "Recompiler.hpp" +#include +#include #include #include #include #include #include +#include +#include +#include +#include +#include #include +#include using namespace ShaderRecompiler; @@ -80,6 +89,46 @@ bool run(const char* name, const std::function& test) { } } +std::vector Recompiled(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array capabilities{1u, 61u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto& words = Recompile(request).spirv.Words(); + return {words.begin(), words.end()}; +} + +std::size_t ExponentMasks(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module"); + std::set exponents; + std::size_t masks = 0; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpConstant && count == 4 && words[offset + 3] == 0x7f800000u) exponents.insert(words[offset + 2]); + if (opcode == spv::OpBitwiseAnd && count == 5 && (exponents.contains(words[offset + 3]) || exponents.contains(words[offset + 4]))) ++masks; + offset += count; + } + return masks; +} + } int main() { @@ -252,5 +301,79 @@ int main() { return loop(true) && !loop(false); }); + passed &= run("a product behind its round-to-nearest tiny-product guard needs no flush, one behind another guard does", [] { + Builder b; + const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); }; + const auto bitsOf = [&](IrValue& number) -> IrValue& { return b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&number}); }; + const auto guarded = [&](IrValue& lhs, IrValue& rhs, IrValue& guardLhs, IrValue& guardRhs, std::uint32_t threshold) -> IrValue& { + auto& product = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& scaled = b.emit(IrOpcode::FPMul32, IrType::F32, {&b.emit(IrOpcode::FPMul32, IrType::F32, {&guardLhs, &b.constant(0x5f800000u)}), &guardRhs}); + auto& magnitude = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bitsOf(scaled), &b.constant(0x7fffffffu)}); + auto& tiny = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.emit(IrOpcode::ISub32, IrType::U32, {&magnitude, &b.constant(1u)}), &b.constant(threshold)}); + auto& sign = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::BitwiseXor32, IrType::U32, {&bitsOf(lhs), &bitsOf(rhs)}), &b.constant(0x80000000u)}); + return b.flush(bitsOf(b.emit(IrOpcode::SelectF32, IrType::F32, {&tiny, &value(sign), &product}))); + }; + auto& x = value(b.flush(b.unknown())); + auto& y = value(b.flush(b.unknown())); + auto& raw = value(b.unknown()); + IrValue* removed[] = {&guarded(x, y, x, y, 0x207fffffu), &guarded(x, y, y, x, 0x207fffffu)}; + IrValue* kept[] = {&guarded(x, y, x, raw, 0x207fffffu), &guarded(x, y, x, y, 0x20800000u), &guarded(x, raw, x, raw, 0x207fffffu)}; + for (auto* flushed : removed) b.keep(*flushed); + for (auto* flushed : kept) b.keep(*flushed); + const bool counted = b.eliminate() == 2u; + bool flushes = true; + for (auto* flushed : removed) flushes &= !flushed->HasUses(); + for (auto* flushed : kept) flushes &= flushed->HasUses(); + return counted && flushes; + }); + + passed &= run("an arm a select takes only when its bits are NaN needs no flush, one under another value's NaN test does", [] { + Builder b; + auto& x = b.unknown(); + auto& other = b.unknown(); + const auto magnitude = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &b.constant(0x7fffffffu)}); }; + const auto isNan = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&magnitude(bits), &b.constant(0x7f800000u)}); }; + const auto chosen = [&](IrValue& condition, IrValue& arm) -> IrValue& { return b.flush(b.emit(IrOpcode::SelectU32, IrType::U32, {&condition, &arm, &b.flush(b.unknown())})); }; + IrValue* removed[] = { + &chosen(isNan(x), x), + &chosen(isNan(x), b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&x, &b.constant(0x00400000u)})), + &chosen(b.emit(IrOpcode::FPIsNan32, IrType::Bool, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&x})}), x), + }; + IrValue* kept[] = { + &chosen(isNan(other), x), + &chosen(b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&b.constant(0x7f800000u), &magnitude(x)}), x), + }; + for (auto* flushed : removed) b.keep(*flushed); + for (auto* flushed : kept) b.keep(*flushed); + const bool counted = b.eliminate() == 3u; + bool flushes = true; + for (auto* flushed : removed) flushes &= !flushed->HasUses(); + for (auto* flushed : kept) flushes &= flushed->HasUses(); + return counted && flushes; + }); + + passed &= run("the sign or exponent bits of any value need no flush, a mask that keeps significand bits does", [] { + Builder b; + auto& sign = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x80000000u)})); + auto& exponent = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.constant(0xff800000u), &b.unknown()})); + auto& significand = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x807fffffu)})); + b.keep(sign); + b.keep(exponent); + b.keep(significand); + return b.eliminate() == 2u && !sign.HasUses() && !exponent.HasUses() && significand.HasUses(); + }); + + passed &= run("two chained v_mul_f32 keep only the input flush of the first source", [] { + const std::array code{ + 0x10020100u, + 0x10040301u, + 0xe0700000u, 0x80000200u, + 0xbf810000u, + }; + const auto masks = ExponentMasks(Recompiled(code)); + if (masks != 1u) std::fprintf(stderr, " %zu f32 exponent masks, expected 1\n", masks); + return masks == 1u; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index a427dfcc29..006e4efd53 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"); @@ -381,8 +458,8 @@ void RunDepthSurfaceReuseTests() { const auto otherWithA = pairing(otherDepth, stencilA); Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); { - constexpr std::uint64_t arrayBase = 0x80000000; - constexpr std::uint64_t htile = 0x81000000; + constexpr std::uint64_t arrayBase = 0x30000000; + constexpr std::uint64_t htile = 0x31000000; constexpr VkExtent2D extent{1024, 512}; const auto sliceHtile = HtileMetadataBytes(extent); DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; 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..1f3594c1c8 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"); @@ -2414,6 +2569,7 @@ alignas(4096) std::array cmaskMemory{}; void cmaskTests() { using AgcDriver::Graphics::DecodeColorBuffer; + using AgcDriver::Graphics::CmaskLayout; auto queue = makeState(); queue.context[0x31c] |= 0x2000; const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); @@ -2434,16 +2590,59 @@ void cmaskTests() { Require(DecodeColorBuffer(makeState().context, 0).cmaskAddress == 0, "a target without FAST_CLEAR got a CMASK"); using AgcDriver::Graphics::CmaskBytes; Require(CmaskBytes(1920, 1080) == 0x6000 && CmaskBytes(960, 544) == 0x2000 && CmaskBytes(800, 450) == 0x1000 && CmaskBytes(1024, 512) == 0x1000 && CmaskBytes(3840, 2160) == 0x14000, "a CMASK does not span whole 1024x512 metablocks"); - auto mipmapped = queue; - mipmapped.context[0x3b0] |= 1u << 28u; - expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); constexpr std::size_t blockBytes = 65536u; static std::vector mipStorage(16u * blockBytes); const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; - auto higherMip = queue; - higherMip.context[0x318] = static_cast(mipBase >> 8u); - higherMip.context[0x390] = static_cast(mipBase >> 40u); - higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + auto tailMipmapped = queue; + tailMipmapped.context[0x3b8] = 0x0dc6c000u; + tailMipmapped.context[0x318] = static_cast(mipBase >> 8u); + tailMipmapped.context[0x390] = static_cast(mipBase >> 40u); + tailMipmapped.context[0x3b0] |= 1u << 28u; + const auto tailBaseMipTarget = DecodeColorBuffer(tailMipmapped.context, 0); + Require(tailBaseMipTarget.cmaskFastClear && tailBaseMipTarget.cmaskAddress == cmaskAddress && tailBaseMipTarget.cmaskBytes == cmaskMemory.size(), "tail-bound mip 0 of a mipmapped fast-clear target did not decode at CMASK base"); + auto arrayTarget = queue; + arrayTarget.context[0x3b8] |= 1u; + expectFailure([&] { DecodeColorBuffer(arrayTarget.context, 0); }, "3D or array"); + auto volumeTarget = queue; + volumeTarget.context[0x3b8] = (queue.context.at(0x3b8) & ~0x03000000u) | 0x02000000u; + expectFailure([&] { DecodeColorBuffer(volumeTarget.context, 0); }, "3D or array"); + static std::vector cmaskChainStorage(16u * CmaskLayout::Alignment); + const auto cmaskChainBase = (reinterpret_cast(cmaskChainStorage.data()) + CmaskLayout::Alignment - 1u) / CmaskLayout::Alignment * CmaskLayout::Alignment; + auto mipmapped = queue; + mipmapped.context[0x3b8] = 0x0dc6c000u; + mipmapped.context[0x318] = static_cast(mipBase >> 8u); + mipmapped.context[0x390] = static_cast(mipBase >> 40u); + mipmapped.context[0x31f] = static_cast(cmaskChainBase >> 8u); + mipmapped.context[0x398] = static_cast(cmaskChainBase >> 40u); + mipmapped.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + const auto baseMipTarget = DecodeColorBuffer(mipmapped.context, 0); + Require(baseMipTarget.cmaskFastClear && baseMipTarget.cmaskAddress == cmaskChainBase + 0x2000 && baseMipTarget.cmaskBytes == 4096, "base mip 0 of a mipmapped fast-clear target did not match addrlib CMASK offset"); + static std::vector largeMipStorage(50331648u + blockBytes); + const auto largeMipBase = (reinterpret_cast(largeMipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + static std::vector largeCmaskChainStorage(26u * CmaskLayout::Alignment); + const auto largeCmaskChainBase = (reinterpret_cast(largeCmaskChainStorage.data()) + CmaskLayout::Alignment - 1u) / CmaskLayout::Alignment * CmaskLayout::Alignment; + auto largeMipmapped = queue; + largeMipmapped.context[0x3b8] = 0x0dc6c000u; + largeMipmapped.context[0x318] = static_cast(largeMipBase >> 8u); + largeMipmapped.context[0x390] = static_cast(largeMipBase >> 40u); + largeMipmapped.context[0x31f] = static_cast(largeCmaskChainBase >> 8u); + largeMipmapped.context[0x398] = static_cast(largeCmaskChainBase >> 40u); + largeMipmapped.context[0x3b0] = (7u << 28u) | (4095u << 14u) | 2047u; + const auto largeBaseMipTarget = DecodeColorBuffer(largeMipmapped.context, 0); + Require(largeBaseMipTarget.cmaskFastClear && largeBaseMipTarget.cmaskAddress == largeCmaskChainBase + 0x9000 && largeBaseMipTarget.cmaskBytes == 16u * CmaskLayout::Alignment, "multiblock base mip 0 did not match addrlib CMASK offset"); + auto nonPowerOfTwo = queue; + nonPowerOfTwo.context[0x3b8] = 0x0dc6c000u; + nonPowerOfTwo.context[0x318] = static_cast(largeMipBase >> 8u); + nonPowerOfTwo.context[0x390] = static_cast(largeMipBase >> 40u); + nonPowerOfTwo.context[0x31f] = static_cast(largeCmaskChainBase >> 8u); + nonPowerOfTwo.context[0x398] = static_cast(largeCmaskChainBase >> 40u); + nonPowerOfTwo.context[0x3b0] = (1u << 28u) | (2048u << 14u) | 1023u; + const auto nonPowerOfTwoTarget = DecodeColorBuffer(nonPowerOfTwo.context, 0); + Require(nonPowerOfTwoTarget.cmaskFastClear && nonPowerOfTwoTarget.cmaskAddress == largeCmaskChainBase + 0x2000 && nonPowerOfTwoTarget.cmaskBytes == 6u * CmaskLayout::Alignment, "non-power-of-two base mip 0 did not round up mip dimensions"); + auto smallBppMipmapped = mipmapped; + smallBppMipmapped.context[0x31c] = (mipmapped.context[0x31c] & ~0x7cu) | 4u; + expectFailure([&] { DecodeColorBuffer(smallBppMipmapped.context, 0); }, "8 or 16 bpp"); + auto higherMip = mipmapped; higherMip.context[0x31b] = 1u << 26u; const auto higherTarget = DecodeColorBuffer(higherMip.context, 0); Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK"); @@ -2629,6 +2828,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 +3029,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 +3070,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); @@ -2843,9 +3115,15 @@ void multisampleTests() { } queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + for (const auto [packedInfo, name] : {std::pair{6u << 2u, "10_11_11 unorm"}, std::pair{(7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u, "11_11_10 float"}}) { + auto packed = rasterizedResolve; + for (const auto offset : {0x31cu, 0x32bu}) packed.context[offset] = (packed.context[offset] & ~0x41f7cu) | packedInfo; + expectFailure([&] { DecodeColorMetadataPass(packed); }, std::string("CB resolves of ") + name + " color targets"); + } 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 +3140,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 +3156,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 +3270,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 +3782,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 +3831,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 +3864,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 +3999,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 +4062,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 +4193,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 +4372,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 +4604,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 +4871,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 +4926,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 +5052,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 +5341,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 +5441,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 +5637,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 +5663,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 +5744,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; @@ -5115,7 +5895,78 @@ void barycentricEmulationTests() { expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, plain, target, limits)); }, "does not emulate barycentrics"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, std::nullopt)); }, "geometry shaders are unavailable"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, GeometryStageLimits{64u, 12u, 256u, 1024u, 128u})); }, "exceeds device limits"); + + const std::array tessellationCapabilities{spv::CapabilityShader, spv::CapabilityTessellation}; + SpirvTarget tessellationTarget = target; + tessellationTarget.supportedCapabilities = tessellationCapabilities; + tessellationTarget.tessellation = TessellationTargetLimits{32u, 128u, 128u, 120u, 4096u, 128u, 128u}; + const auto modes = [](const RecompileResult& result) { + std::vector values; + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpExecutionMode) values.push_back(words[at + 2]); + } + return values; + }; + auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u); + Require(modes(patches.control) == std::vector{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain"); + Require(modes(patches.evaluation) == std::vector{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain"); + const std::array patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches"); + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + ValidateShaders(patchShaders, state, subgroup, false); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { + state.topology = topology; + expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list"); + } + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const std::array plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}}; + expectFailure([&] { ValidateShaders(plainPatchShaders, state, subgroup, false); }, "stage count"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u)); }, "does not emulate barycentrics"); + auto noTessellation = tessellationTarget; + noTessellation.tessellation.reset(); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u)); }, "tessellation shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u)); }, "tessellation shaders are unavailable"); + auto noCapability = tessellationTarget; + noCapability.supportedCapabilities = std::span(capabilities).first(1); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u)); }, "tessellation capability is unavailable"); + auto smallPatches = tessellationTarget; + smallPatches.tessellation->maxEvaluationOutputComponents = 4u; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u)); }, "exceeds device limits"); + + RecompileResult clipped; + clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2}); + clipped.parameterExports = {0u}; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u)); }, "need shaderClipDistance"); + const std::array clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; + auto clipTarget = tessellationTarget; + clipTarget.supportedCapabilities = clipCapabilities; + auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u); + const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true; + } + return false; + }; + const auto variableBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + std::set variables; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpVariable) variables.insert(words[at + 2]); + } + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin && variables.contains(words[at + 1])) return true; + } + return false; + }; + Require(!memberBuiltIn(patches.control, spv::BuiltInClipDistance) && !memberBuiltIn(patches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage declares clip distances the vertex shader does not write"); + Require(!memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && !memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance) && variableBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && variableBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "generated tessellation stages do not forward clip distances as separate built-in variables"); + const std::array clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true); vertex.parameterExports.clear(); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u)); }, "not exported by the vertex shader"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } @@ -5246,6 +6097,23 @@ void registeredReadableCoverTests() { ::operator delete(memory, std::align_val_t{block}); } +std::size_t VariableBuiltIns(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) { + std::size_t count = 0; + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpDecorate && (words[at] >> 16u) == 4u && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin) ++count; + } + return count; +} + +bool MemberBuiltIn(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true; + } + return false; +} + void rectListTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -5279,17 +6147,35 @@ void rectListTests() { expectFailure([&] { ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); }, "stage count"); } } + { + RecompileResult clipped; + clipped.spirv = makeModule({.clipDistanceLength = 2}); + const std::array clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; + auto clipTarget = target; + clipTarget.supportedCapabilities = clipCapabilities; + auto clippedRect = BuildRectListShaders(clipped, fragment, clipTarget); + for (const auto* stage : {&clippedRect.control, &clippedRect.lastVertexControl, &clippedRect.evaluation}) { + Require(!MemberBuiltIn(*stage, spv::BuiltInClipDistance) && VariableBuiltIns(*stage, spv::BuiltInClipDistance) == 2u, "a rect-list stage does not take and forward the vertex clip distances through variables of their own, as the vertex shader writes them"); + } + const std::array clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedRect.control, 0}, {ShaderStage::TessellationEvaluation, &clippedRect.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + auto queue = makeState(); + queue.userConfig[0x242] = 17; + ValidateShaders(clippedShaders, DecodeState(queue), VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false, false, false, false, false, false, true); + } vertex.spirv = makeModule({.parameterOutput = true}); fragment.spirv = makeModule({.fragment = true, .rectParameters = true}); vertex.parameterExports = {0}; 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 +6415,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 +6433,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 +6457,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 +6506,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 +7040,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"); } @@ -6355,13 +7293,78 @@ void unorm10_11_11TargetTests() { return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false); }; Require(validate(packed, state) == std::set{0u}, "the packed export did not reach attachment 0"); - expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed 10_11_11 unorm attachments"); + expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed color attachments"); auto plainState = state; plainState.colors[0].packing = ColorExportPacking::None; expectFailure([&] { validate(packed, plainState); }, "float4 colors"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; - expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no 10_11_11 unorm packing path"); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); +} + +void float11_11_10TargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~0x1f7cu) | (7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R32_UINT && state.color.elementBytes == 4 && state.color.bytes == colorMemory.size() && state.color.componentMapping == 0xc6u && state.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target did not decode as a packed R32_UINT attachment with blue in the low bits"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Float11_11_10 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 11_11_10 float packing alone"); + Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u, "export 0 did not reverse its components into the 11_11_10 float target"); + auto changed = queue; + changed.context[0x31c] &= ~0x40000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets"); + for (const auto swap : {0u, 1u, 3u}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap)); + } + for (const auto number : {0u, 4u}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~0x700u) | (number << 8u) | 0x8000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type " + std::to_string(number) + " component swap 2"); + } + changed = queue; + changed.context[0x1e0] = 0x60010101u; + for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into an 11_11_10 float color target"); + changed = queue; + changed.context[0x8e] = 6; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of an 11_11_10 float color target"); + changed.context[0x8e] = 7; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an RGB write of an 11_11_10 float target was refused"); + for (const auto format : {5u, 6u}) { + changed = queue; + changed.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color export format " + std::to_string(format) + " into an 11_11_10 float target"); + } + changed = queue; + changed.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an FP16_ABGR export into an 11_11_10 float target was refused"); + changed = queue; + changed.context[0x31c] |= 0x10000000u; + changed.context[0x325] = 0x1234; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 11_11_10 float color targets"); + + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto request = packedExportRequest(queue, state, code); + auto unorm = request; + unorm.context.pixel->targetExportPacking[0] = ColorExportPacking::Unorm10_11_11; + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 11_11_10 float packing was lost in serialization"); + const auto packed = ShaderRecompiler::Recompile(request); + const auto unormPacked = ShaderRecompiler::Recompile(unorm); + Require(packed.variantId == unormPacked.variantId && packed.PipelineVariantId() != unormPacked.PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization of the shared artifact"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the packed float export did not reach attachment 0"); + auto invalid = request; + invalid.context.pixel->targetExportPacking[0] = static_cast(3); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); } AgcDriver::QueueState dualSourceQueue() { @@ -6614,6 +7617,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 +7952,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 +8229,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 +8552,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 +8652,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 +8690,10 @@ int main(int argc, char** argv) { RunGuestLeaseWaitTests(); ResourceContentKeyTests(false); stateTests(); + pipelineStoreLazyBufferPoolTests(); retainedPixelStageTests(); pipelineLibrarySetTests(); + pipelineLibraryClearWaitsForBuildTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); @@ -7324,6 +8705,7 @@ int main(int argc, char** argv) { memoryValueFillTests(); ZExportTests(); unorm10_11_11TargetTests(); + float11_11_10TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); earlyFragmentTestsTests(); @@ -7337,8 +8719,10 @@ int main(int argc, char** argv) { ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); + Color565TargetTests(); Color1555TargetTests(); BlendBypassTests(); + UnclampedNormalizedColorTests(); CompactedExportTests(); ReversedComponentOrderTests(); ReversedComponentOrderBlendTests(); @@ -7352,9 +8736,11 @@ int main(int argc, char** argv) { uint8x4TargetTests(); multisampleTests(); MsaaEnableSingleSampleTests(); + multisampledDepthTests(); clipDistanceTests(); framebufferLayerTests(); UnwrittenSampleLocationTests(); + depthCopyFormatTests(); quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); @@ -7372,7 +8758,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/GuardedMemoryLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp new file mode 100644 index 0000000000..e20b23d8d5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp @@ -0,0 +1,370 @@ +#define SPV_ENABLE_UTILITY_CODE +#include +#undef SPV_ENABLE_UTILITY_CODE +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include "PipelineSpecialization.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +struct LoadCase { + ResourceKind kind; + IrOpcode opcode; + IrType type; + std::uint32_t bits = 32; + std::uint32_t components = 1; +}; + +std::vector emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) { + IrProgram program; + program.SetWaveSize(32); + program.Resources().stage = IrShaderStage::Compute; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + program.Metadata().blockInfo.emplace_back(); + const auto constant = [&](std::uint32_t number) -> IrValue& { + auto& value = program.CreateValue(IrOpcode::Void, IrType::U32); + value.SetImmediateU32(number); + return value; + }; + const auto emit = [&](IrOpcode opcode, IrType type, std::initializer_list arguments) -> IrValue& { + auto& value = program.CreateValue(opcode, type); + for (auto* argument : arguments) value.AddArgument(argument); + block.AppendInstruction(&value); + return value; + }; + auto& enabled = program.CreateValue(IrOpcode::Void, IrType::Bool); + enabled.SetImmediateBool(active); + auto& zero = constant(0); + auto& address = constant(offset); + MemoryInfo memory; + memory.kind = test.kind; + memory.dataBits = test.bits; + memory.dataDwords = test.components; + memory.componentCount = test.components; + program.Resources().memoryInfo.push_back(memory); + if (test.kind == ResourceKind::Scratch) { + program.Info().scratchDwords = 16; + auto& handle = emit(IrOpcode::GetScratchResource, IrType::AddressResource, {}); + emit(test.opcode, test.type, {&handle, &address, &zero, &enabled}); + } else if (test.kind == ResourceKind::Buffer) { + program.Resources().memoryInfo.front().coherent = true; + program.Info().buffers.emplace_back().read = true; + program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Buffers, {0}}); + program.Metadata().bindings.pushDataStartDword = 0; + program.Metadata().bindings.memoryOffsetCount = 1; + auto& size = constant(records); + auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero}); + emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled}); + } else { + if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}}); + emit(test.opcode, test.type, {&address, &enabled}); + } + ShaderComputeInputInfo compute; + compute.threadsNum[0] = 32; + compute.threadsNum[1] = 1; + compute.threadsNum[2] = 1; + compute.ldsSizeDwords = 16; + compute.scratchSizeDwords = 16; + compute.hostSubgroupSize = 32; + compute.waveSize = 32; + ShaderStageInputInfo inputs; + inputs.compute = &compute; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 32; + target.robustBufferAccess2 = robust; + return SpirvEmitter().Emit(program, inputs, bindings, target); +} + +class AccessTrace { +public: + AccessTrace(const std::vector& module, std::uint32_t misalignment = 0, bool valid = true) + : words(module), misalignment(misalignment), valid(valid) { + require(words.size() >= 5, "SPIR-V module header is missing"); + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16; + require(count != 0 && count <= words.size() - offset, "SPIR-V instruction is truncated"); + const auto instruction = std::span(words).subspan(offset, count); + const auto opcode = static_cast(instruction[0] & 0xffff); + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(instruction[type ? 2 : 1], instruction); + if (opcode == spv::OpName) names.emplace(instruction[1], std::string(reinterpret_cast(instruction.data() + 2))); + if (opcode == spv::OpDecorate && instruction[2] == spv::DecorationSpecId) specializations.emplace(instruction[1], instruction[3]); + if (opcode == spv::OpLabel) labels.emplace(instruction[1], offset); + if (opcode == spv::OpEntryPoint) entryFunction = instruction[2]; + offset += count; + } + } + + std::vector Run() { + std::size_t cursor = 0; + bool entry = false; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) { + const auto opcode = static_cast(words[offset] & 0xffff); + if (opcode == spv::OpFunction) entry = words[offset + 2] == entryFunction; + if (entry && opcode == spv::OpLabel) { cursor = offset; break; } + } + require(cursor != 0, "SPIR-V entry block is missing"); + std::vector reads; + std::uint32_t previous = 0; + std::uint32_t current = 0; + for (std::size_t steps = 0; steps < 4096; ++steps) { + const auto instruction = std::span(words).subspan(cursor, words[cursor] >> 16); + const auto opcode = static_cast(instruction[0] & 0xffff); + if (opcode == spv::OpLabel) current = instruction[1]; + if (opcode == spv::OpPhi) { + bool found = false; + for (std::size_t index = 3; index + 1 < instruction.size(); index += 2) { + if (instruction[index + 1] == previous) { phiValues[instruction[2]] = instruction[index]; found = true; } + } + require(found, "SPIR-V phi has no incoming edge for the executed path"); + } + if (opcode == spv::OpLoad && isGuestPointer(instruction[3])) { + const auto pointer = definition(instruction[3]); + require((pointer[0] & 0xffff) == spv::OpAccessChain, "guest read is not an indexed access"); + reads.push_back(value(pointer.back())); + if (rootName(instruction[3]) == "scratch_dwords") require(pointerStorage(instruction[3]) == spv::StorageClassFunction, "scratch selection accessed non-private storage"); + if (rootName(instruction[3]).starts_with("buffers")) require(instruction.size() == 5 && (instruction[4] & spv::MemoryAccessVolatileMask), "coherent buffer read lost its volatile access"); + } + if (opcode == spv::OpBranch || opcode == spv::OpBranchConditional) { + const auto destination = opcode == spv::OpBranch ? instruction[1] : instruction[value(instruction[1]) ? 2 : 3]; + previous = current; + cursor = labels.at(destination); + continue; + } + if (opcode == spv::OpReturn) return reads; + require(opcode != spv::OpFunctionCall && opcode != spv::OpSwitch && opcode != spv::OpLoopMerge, "access fixture unexpectedly requires a call, switch or loop"); + cursor += instruction.size(); + require(cursor < words.size(), "SPIR-V execution fell outside the function"); + } + throw std::runtime_error("SPIR-V access trace exceeded its bounded instruction budget"); + } + +private: + std::span definition(std::uint32_t id) const { + const auto found = definitions.find(id); + require(found != definitions.end(), "SPIR-V access condition refers to an undefined value"); + return found->second; + } + + std::string rootName(std::uint32_t id) const { + const auto instruction = definition(id); + if ((instruction[0] & 0xffff) == spv::OpAccessChain) return rootName(instruction[3]); + const auto found = names.find(id); + return found == names.end() ? std::string() : found->second; + } + + std::uint32_t pointerStorage(std::uint32_t id) const { + const auto instruction = definition(id); + if ((instruction[0] & 0xffff) == spv::OpAccessChain) return pointerStorage(instruction[3]); + require((instruction[0] & 0xffff) == spv::OpVariable, "access pointer has no variable root"); + return instruction[3]; + } + + bool isGuestPointer(std::uint32_t id) const { + const auto name = rootName(id); + return name == "lds_dwords" || name == "gds" || name == "scratch_dwords" || name.starts_with("buffers"); + } + + std::uint32_t value(std::uint32_t id) const { + const auto instruction = definition(id); + const auto opcode = static_cast(instruction[0] & 0xffff); + const auto argument = [&](std::size_t index) { return value(instruction[index]); }; + switch (opcode) { + case spv::OpConstantTrue: return 1; + case spv::OpConstantFalse: case spv::OpConstantNull: return 0; + case spv::OpConstant: return instruction[3]; + case spv::OpSpecConstant: { + const auto spec = specializations.at(id); + if (spec == PipelineSpecialization::BufferBase) return 0; + if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000; + if (spec == PipelineSpecialization::BufferBase + 2) return valid; + if (spec == PipelineSpecialization::BufferBase + 3) return misalignment; + if (spec == PipelineSpecialization::PushDataOffset) return 0; + throw std::runtime_error("unexpected specialization in access fixture"); + } + case spv::OpSelect: return argument(3) ? argument(4) : argument(5); + case spv::OpPhi: return value(phiValues.at(id)); + case spv::OpIAdd: return argument(3) + argument(4); + case spv::OpISub: return argument(3) - argument(4); + case spv::OpIMul: return argument(3) * argument(4); + case spv::OpShiftLeftLogical: + require(argument(4) < 32, "undefined shift in access condition"); + return argument(3) << argument(4); + case spv::OpShiftRightLogical: + require(argument(4) < 32, "undefined shift in access condition"); + return argument(3) >> argument(4); + case spv::OpBitwiseAnd: return argument(3) & argument(4); + case spv::OpBitwiseOr: return argument(3) | argument(4); + case spv::OpBitFieldUExtract: { + const auto count = argument(5); + require(count <= 32 && argument(4) <= 32 - count, "invalid bitfield in access condition"); + return (argument(3) >> argument(4)) & (count == 32 ? 0xffffffffu : (1u << count) - 1); + } + case spv::OpIEqual: return argument(3) == argument(4); + case spv::OpINotEqual: return argument(3) != argument(4); + case spv::OpULessThan: return argument(3) < argument(4); + case spv::OpULessThanEqual: return argument(3) <= argument(4); + case spv::OpUGreaterThanEqual: return argument(3) >= argument(4); + case spv::OpLogicalAnd: return argument(3) && argument(4); + case spv::OpLogicalOr: return argument(3) || argument(4); + case spv::OpLogicalNot: return !argument(3); + case spv::OpArrayLength: + require(isGuestPointer(instruction[3]), "unknown array length in access fixture"); + return 16; + case spv::OpLoad: + require(!isGuestPointer(instruction[3]), "guest data unexpectedly controls a fixture access"); + require(rootName(instruction[3]) == "vsharp" || rootName(instruction[3]).find("subgroup") != std::string::npos, "unknown input to access condition"); + return 0; + default: throw std::runtime_error("unsupported opcode in access condition: " + std::to_string(opcode)); + } + } + + const std::vector& words; + std::uint32_t misalignment; + bool valid; + std::uint32_t entryFunction = 0; + std::map> definitions; + std::map names; + std::map specializations; + std::map labels; + std::map phiValues; +}; + +constexpr std::uint32_t Unreachable = 0xfffffffeu; + +std::uint32_t ldsLength(const std::vector& words) { + std::map> definitions; + std::uint32_t variable = 0; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) { + const auto instruction = std::span(words).subspan(offset, words[offset] >> 16); + const auto opcode = static_cast(instruction[0] & 0xffff); + if (opcode == spv::OpName && std::string(reinterpret_cast(instruction.data() + 2)) == "lds_dwords") variable = instruction[1]; + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(instruction[type ? 2 : 1], instruction); + } + require(variable != 0, "LDS array is missing"); + const auto pointer = definitions.at(definitions.at(variable)[1]); + const auto array = definitions.at(pointer[3]); + require((array[0] & 0xffff) == spv::OpTypeArray, "LDS variable is not an array"); + return definitions.at(array[3])[3]; +} + +void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) { + const auto module = emitLoad(test, active, offset, records, robust); + const auto actual = AccessTrace(module, misalignment, valid).Run(); + if (actual != expected) { + std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:"; + for (const auto index : actual) detail += " " + std::to_string(index); + detail += "; expected:"; + for (const auto index : expected) detail += " " + std::to_string(index); + throw std::runtime_error(detail); + } +} + +void ldsLoads(IrOpcode opcode, IrType type, std::uint32_t bits, std::uint32_t components) { + const LoadCase test{ResourceKind::Lds, opcode, type, bits, components}; + require(ldsLength(emitLoad(test, true, 8, 64, false)) == 16u, "LDS array declares more than the program's dwords"); + for (const bool robust : {false, true}) { + expectReads(test, robust, false, 8, {}); + expectReads(test, robust, true, 84, {}); + std::vector indices; + for (std::uint32_t index = 0; index < components; ++index) indices.push_back(2 + index); + expectReads(test, robust, true, 8, indices); + expectReads(test, robust, true, 60, {15}); + } +} + +} + +int main() { + unsigned failures = 0; + const auto run = [&](std::string_view name, const auto& test) { + try { + test(); + std::cout << "guarded memory loads: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + std::cout << "guarded memory loads: " << name << ": " << error.what() << '\n'; + } + }; + run("LDS word keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32, IrType::U32, 32, 1); }); + run("LDS subword keeps its branch", [] { + ldsLoads(IrOpcode::LoadSharedU8, IrType::U8, 8, 1); + ldsLoads(IrOpcode::LoadSharedU16, IrType::U16, 16, 1); + }); + run("LDS wide keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32x4, IrType::U32x4, 32, 4); }); + run("GDS word", [] { + const LoadCase test{ResourceKind::Gds, IrOpcode::LoadSharedU32, IrType::U32, 32, 1}; + expectReads(test, false, false, 8, {}); + expectReads(test, false, true, 64, {}); + expectReads(test, false, true, 8, {2}); + expectReads(test, true, false, 8, {Unreachable}); + expectReads(test, true, true, 64, {Unreachable}); + expectReads(test, true, true, 8, {2}); + expectReads(test, true, true, 60, {15}); + }); + run("coherent buffer bounds", [] { + const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32}; + for (const bool robust : {false, true}) expectReads(test, robust, false, 8, {}); + expectReads(test, false, true, 64, {}, 128); + expectReads(test, false, true, 60, {}, 32); + expectReads(test, false, true, 8, {}, 64, 0, false); + expectReads(test, true, true, 64, {Unreachable, Unreachable}, 128); + expectReads(test, true, true, 60, {Unreachable, Unreachable}, 32); + expectReads(test, true, true, 8, {Unreachable, Unreachable}, 64, 0, false); + }); + run("coherent buffer high word", [] { + const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32}; + expectReads(test, false, true, 8, {2}); + expectReads(test, false, true, 8, {2, 3}, 64, 1); + expectReads(test, false, true, 60, {}, 64, 1); + expectReads(test, true, true, 8, {2, 2}); + expectReads(test, true, true, 8, {2, 3}, 64, 1); + expectReads(test, true, true, 60, {Unreachable, Unreachable}, 64, 1); + }); + run("private scratch selection", [] { + const LoadCase test{ResourceKind::Scratch, IrOpcode::LoadAddressU32, IrType::U32}; + for (const bool robust : {false, true}) { + expectReads(test, robust, false, 8, {0}); + expectReads(test, robust, true, 64, {0}); + expectReads(test, robust, true, 8, {2}); + } + }); + if (failures != 0) { + std::cerr << "guarded memory loads: a guest load reached a location the guarded program could not\n"; + return 1; + } + std::cout << "Guarded memory load tests passed\n"; + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp new file mode 100644 index 0000000000..465575f5dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp @@ -0,0 +1,82 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +std::unique_ptr device; +constexpr std::array Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; +} + +extern "C" int APS5_VABI Begin_nid_no_patch() { + try { + device = OpenVulkanTestDevice(); + if (!device || !GuestMemoryLifetimeHasHostImports(device->Device())) { + device.reset(); + std::fflush(stdout); + std::exit(VulkanTestSkipped); + } + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "converted guest Vulkan initialization failed: %s\n", error.what()); + std::exit(1); + } +} + +extern "C" int APS5_VABI Fill_nid_no_patch(void* pointer, int gpu) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(pointer); + if (!range.readable || !range.writable || range.gpu != (gpu != 0)) { + std::fputs("converted guest mapping does not match its requested CPU and GPU access\n", stderr); + return 1; + } + } + if (!device->FillBuffer(reinterpret_cast(pointer), 4096, Pattern)) return 1; + AgcDriver::Graphics::Context probe{}; + probe.device = device->Device(); + probe.hostImportAlignment = 1; + return !AgcDriver::Graphics::HostImportCovers(probe, reinterpret_cast(pointer), 65536); +} + +extern "C" int APS5_VABI Retired_nid_no_patch(const void* pointer) { + AgcDriver::Graphics::Context probe{}; + probe.device = device->Device(); + probe.hostImportAlignment = 1; + const bool imported = AgcDriver::Graphics::HostImportCovers(probe, reinterpret_cast(pointer), 65536); + if (imported) std::fputs("converted guest observed a stale Vulkan host import\n", stderr); + return imported; +} + +extern "C" int APS5_VABI Verify_nid_no_patch(const void* pointer, int zero) { + const auto* bytes = static_cast(pointer); + for (std::size_t offset = 0; offset < 4096; ++offset) { + const auto expected = zero ? std::uint8_t{0} : reinterpret_cast(Pattern.data())[offset % sizeof(Pattern)]; + if (bytes[offset] != expected) { + std::fprintf(stderr, "converted guest memory mismatch at byte %zu: expected %u, observed %u\n", + offset, unsigned(expected), unsigned(bytes[offset])); + return 1; + } + } + return 0; +} + +extern "C" [[noreturn]] void APS5_VABI Finish_nid_no_patch(int result) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device->WaitIdle(); + } + device.reset(); + std::printf("converted guest memory/Vulkan lifetime result=%d\n", result); + std::fflush(stdout); + std::exit(result); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp new file mode 100644 index 0000000000..863b8ca67f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GUESTMEMORYLIFETIMESUPPORT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GUESTMEMORYLIFETIMESUPPORT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +static inline bool GuestMemoryLifetimeHasHostImports(VkDevice device) { + if (std::getenv("APS5_NO_HOST_IMPORT") != nullptr) { + std::puts("skipped, host imports are disabled by APS5_NO_HOST_IMPORT"); + return false; + } +#ifdef _WIN32 + constexpr const char* libraryName = "vulkan-1.dll"; +#else + constexpr const char* libraryName = "libvulkan.so.1"; +#endif + const std::unique_ptr library(SDL_LoadObject(libraryName), &SDL_UnloadObject); + if (!library) throw std::runtime_error(std::string("cannot load Vulkan for the host import capability query: ") + SDL_GetError()); + const auto deviceProc = reinterpret_cast(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); + if (deviceProc == nullptr) throw std::runtime_error(std::string("cannot resolve vkGetDeviceProcAddr for the host import capability query: ") + SDL_GetError()); + if (deviceProc(device, "vkGetMemoryHostPointerPropertiesEXT") == nullptr) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return false; + } + return true; +} + +#endif 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/HostImportLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp new file mode 100644 index 0000000000..c507b67de3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -0,0 +1,803 @@ +#include "HostImportLifetime.hpp" +#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/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::GuestMemory::GpuMutex; + +struct ImportState { + VkBuffer buffer; + VkDeviceMemory memory; + std::uint64_t address; + std::size_t bytes; + std::atomic destroyed{false}; + std::atomic freed{false}; + std::atomic frees{0}; + std::atomic outOfOrder{false}; + std::atomic inaccessible{false}; + std::mutex mutex; + std::condition_variable wake; + bool blockFree = false; + bool freeing = false; + bool allowFree = false; + std::thread::id freeingThread; +}; + +struct Tracking { + PFN_vkGetDeviceProcAddr resolve; + VkDevice device; + std::mutex mutex; + std::map> imports; + std::atomic failAddressLookup{false}; + std::atomic failQueueSubmit{false}; + std::atomic rejectedSubmissions{0}; + std::atomic acceptedSubmissions{0}; + std::vector rejectedFences; + std::shared_ptr lastAllocated; + + void CleanupRejectedFences() { + std::vector fences; + { + std::lock_guard lock(mutex); + fences.swap(rejectedFences); + } + const auto destroy = reinterpret_cast(resolve(device, "vkDestroyFence")); + for (const auto fence : fences) destroy(device, fence, nullptr); + } + + std::shared_ptr Add(const HostImport& import) { + auto state = std::make_shared(); + state->buffer = import.buffer; + state->memory = import.memory; + state->address = import.base; + state->bytes = import.bytes; + std::lock_guard lock(mutex); + imports[import.memory] = state; + return state; + } +}; + +Tracking* tracking = nullptr; + +VKAPI_ATTR void VKAPI_CALL destroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + const auto original = reinterpret_cast(tracking->resolve(device, "vkDestroyBuffer")); + original(device, buffer, allocator); + std::lock_guard lock(tracking->mutex); + for (const auto& [memory, state] : tracking->imports) { + if (state->buffer == buffer && !state->freed.load()) state->destroyed.store(true); + } +} + +VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + std::shared_ptr state; + { + std::lock_guard lock(tracking->mutex); + const auto found = tracking->imports.find(memory); + if (found != tracking->imports.end()) state = found->second; + } + if (state != nullptr) { + if (!state->destroyed.load()) state->outOfOrder.store(true); + if (!RegisteredReadableCovers(state->address, state->bytes) || !AgcDriver::GuestMemory::Accessible(reinterpret_cast(state->address), state->bytes, false)) state->inaccessible.store(true); + std::unique_lock lock(state->mutex); + if (state->blockFree) { + state->freeing = true; + state->freeingThread = std::this_thread::get_id(); + state->wake.notify_all(); + if (!state->wake.wait_for(lock, std::chrono::seconds(2), [&] { return state->allowFree; })) state->outOfOrder.store(true); + } + } + const auto original = reinterpret_cast(tracking->resolve(device, "vkFreeMemory")); + original(device, memory, allocator); + if (state != nullptr) { + if (!RegisteredReadableCovers(state->address, state->bytes) || !AgcDriver::GuestMemory::Accessible(reinterpret_cast(state->address), state->bytes, false)) state->inaccessible.store(true); + state->frees.fetch_add(1); + state->freed.store(true); + } +} + +VKAPI_ATTR VkResult VKAPI_CALL allocateMemory(VkDevice device, const VkMemoryAllocateInfo* allocation, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + const auto original = reinterpret_cast(tracking->resolve(device, "vkAllocateMemory")); + const auto result = original(device, allocation, allocator, memory); + if (result != VK_SUCCESS) return result; + for (auto* next = static_cast(allocation->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* host = reinterpret_cast(next); + auto state = std::make_shared(); + state->buffer = VK_NULL_HANDLE; + state->memory = *memory; + state->address = reinterpret_cast(host->pHostPointer); + state->bytes = static_cast(allocation->allocationSize); + std::lock_guard lock(tracking->mutex); + tracking->imports[*memory] = state; + tracking->lastAllocated = state; + } + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL bindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + { + std::lock_guard lock(tracking->mutex); + if (const auto found = tracking->imports.find(memory); found != tracking->imports.end()) found->second->buffer = buffer; + } + const auto original = reinterpret_cast(tracking->resolve(device, "vkBindBufferMemory")); + return original(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL destroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) { + const auto original = reinterpret_cast(tracking->resolve(device, "vkDestroyFence")); + original(device, fence, allocator); + std::lock_guard lock(tracking->mutex); + std::erase(tracking->rejectedFences, fence); +} + +VKAPI_ATTR VkResult VKAPI_CALL queueSubmit(VkQueue queue, std::uint32_t count, const VkSubmitInfo* submissions, VkFence fence) { + if (tracking->failQueueSubmit.exchange(false)) { + std::lock_guard lock(tracking->mutex); + tracking->rejectedFences.push_back(fence); + tracking->rejectedSubmissions.fetch_add(1); + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + const auto original = reinterpret_cast(tracking->resolve(tracking->device, "vkQueueSubmit")); + const auto result = original(queue, count, submissions, fence); + if (result == VK_SUCCESS) { + std::lock_guard lock(tracking->mutex); + std::erase(tracking->rejectedFences, fence); + tracking->acceptedSubmissions.fetch_add(1); + } + return result; +} + +PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast(destroyFence); + if (std::strcmp(name, "vkQueueSubmit") == 0) return reinterpret_cast(queueSubmit); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(destroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(freeMemory); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(allocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(bindBufferMemory); + if (std::strcmp(name, "vkGetBufferDeviceAddressKHR") == 0 && tracking->failAddressLookup.exchange(false)) return nullptr; + return tracking->resolve(device, name); +} + +void requireFreed(const std::shared_ptr& state) { + Require(state->freed.load(), "guest mutation reached host apply before vkFreeMemory completed"); + Require(state->destroyed.load() && !state->outOfOrder.load() && state->frees.load() == 1, "host import Vulkan objects were destroyed out of order or more than once"); + Require(!state->inaccessible.load(), "guest pages became inaccessible before vkFreeMemory returned"); +} + +class Fixture { +public: + explicit Fixture(const Context& original) : context(original), calls{original.deviceProc, original.device} { + Require(tracking == nullptr, "host import tracking is already active"); + tracking = &calls; + context.deviceProc = deviceProc; + context.functions = nullptr; + std::lock_guard gpu(GpuMutex()); + recorder = std::make_unique(context); + recorder->Activate(); + } + + ~Fixture() { + std::lock_guard gpu(GpuMutex()); + recorder.reset(); + calls.CleanupRejectedFences(); + ClearHostImports(context.device); + context.bufferPool.reset(); + tracking = nullptr; + } + + std::shared_ptr Import(std::uint64_t address, std::size_t bytes) { + std::lock_guard gpu(GpuMutex()); + const auto* imported = HostImportFor(context, address, bytes); + Require(imported != nullptr, "host pointer import was refused for the lifetime test"); + return calls.Add(*imported); + } + + void Cleanup() { + std::lock_guard gpu(GpuMutex()); + recorder->Sync(); + ClearHostImports(context.device); + } + + Context context; + Tracking calls; + std::unique_ptr recorder; +}; + +class Allocation { +public: + Allocation(Fixture& fixture, std::size_t units = 1) : fixture(fixture) { + alignment = static_cast(std::max(65536, fixture.context.hostImportAlignment)); + bytes = alignment * units; + data = ::operator new(bytes, std::align_val_t{alignment}); + std::memset(data, 0x37, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + + ~Allocation() { + try { + if (registered) { + fixture.Cleanup(); + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } + Release(); + } catch (const std::exception& error) { + std::fprintf(stderr, "host import test cleanup failed: %s\n", error.what()); + std::terminate(); + } + } + + std::uint64_t Address() const { return reinterpret_cast(data); } + + void Release() { + if (released) return; + ::operator delete(data, std::align_val_t{alignment}); + released = true; + } + + Fixture& fixture; + void* data = nullptr; + std::size_t alignment = 0; + std::size_t bytes = 0; + bool registered = true; + bool released = false; +}; + +void descriptorFirstImport(Fixture& fixture) { + Require(fixture.calls.lastAllocated == nullptr, "descriptor-only lifetime test did not run before the first host import"); + Allocation allocation(fixture); + std::shared_ptr imported; + { + std::lock_guard gpu(GpuMutex()); + GuestBufferMemory memory(fixture.context); + memory.AddReadable(allocation.Address(), allocation.bytes); + memory.Upload(false); + imported = fixture.calls.lastAllocated; + Require(imported != nullptr && imported->address == allocation.Address(), "descriptor-only upload did not import its allocation"); + std::uint32_t adjustment = 0; + const auto descriptor = memory.Descriptor(allocation.Address(), allocation.bytes, adjustment); + Require(descriptor.buffer == imported->buffer && descriptor.offset == 0 && adjustment == 0, "descriptor-only upload did not bind the tracked import"); + } + Require(HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "descriptor-only import did not persist after its build ended"); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + allocation.Release(); + }); + allocation.registered = false; + } + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "descriptor-only import survived allocation removal"); +} + +class PendingRetirement { +public: + explicit PendingRetirement(Fixture& fixture) : fixture(fixture), first(fixture) { + std::lock_guard gpu(GpuMutex()); + build = std::make_unique(fixture.context); + build->AcquireRegistered(); + build->UploadPrepare(true); + retry = std::make_unique(fixture.context); + retry->AcquireRegistered(); + retry->UploadPrepare(true); + allocation = std::make_unique(fixture); + imported = fixture.Import(allocation->Address(), allocation->bytes); + result = std::make_unique(fixture.context, allocation->bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result->Handle(), 0, allocation->bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation->Address(), allocation->bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Submit(); + Check(fixture.context.Function("vkQueueWaitIdle")(fixture.context.queue), "vkQueueWaitIdle retirement fixture"); + generation = std::make_unique(fixture); + Require(!fixture.recorder->Idle(), "retirement fixture has no submitted batch"); + } + + ~PendingRetirement() { + if (imported != nullptr) { + { + std::lock_guard lock(imported->mutex); + imported->allowFree = true; + } + imported->wake.notify_all(); + } + build.reset(); + retry.reset(); + fixture.Cleanup(); + } + + void VerifyCopy() { + result->Invalidate(); + const auto bytes = result->Bytes(); + Require(std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0x37}; }), "retirement lost the submitted import read"); + } + + Fixture& fixture; + Allocation first; + std::unique_ptr result; + std::unique_ptr allocation; + std::unique_ptr generation; + std::unique_ptr build; + std::unique_ptr retry; + std::shared_ptr imported; +}; + +void deferredRetirement(Fixture& fixture) { + PendingRetirement pending(fixture); + pending.imported->blockFree = true; + { + std::lock_guard gpu(GpuMutex()); + pending.build->UploadFinish(true); + Require(!HostImportCovers(fixture.context, pending.allocation->Address(), pending.allocation->bytes), "deferred retirement left its import cached"); + Require(!pending.imported->freed.load(), "retirement did not keep its import through the pending batch"); + fixture.recorder->Sync(); + fixture.recorder->FlushDeferredReleases(); + } + bool entered = false; + bool onReleaseThread = false; + { + std::unique_lock lock(pending.imported->mutex); + entered = pending.imported->wake.wait_for(lock, std::chrono::seconds(2), [&] { return pending.imported->freeing; }); + onReleaseThread = pending.imported->freeingThread != std::this_thread::get_id(); + } + std::atomic applied{false}; + const auto waitsBefore = LeaseCounters().contentionWaits; + std::promise starting; + auto started = starting.get_future(); + auto mutation = std::async(std::launch::async, [&] { + starting.set_value(); + GuestAllocations::Mutation mutation; + mutation.Unmap(pending.allocation->data, pending.allocation->bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(pending.imported); + applied.store(true); + pending.allocation->Release(); + }); + pending.allocation->registered = false; + }); + started.wait(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (LeaseCounters().contentionWaits == waitsBefore && mutation.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const bool reachedWaiter = LeaseCounters().contentionWaits > waitsBefore; + const bool blocked = mutation.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout; + const bool premature = applied.load(); + const bool registered = RegisteredReadableCovers(pending.allocation->Address(), pending.allocation->bytes); + { + std::lock_guard lock(pending.imported->mutex); + pending.imported->allowFree = true; + } + pending.imported->wake.notify_all(); + mutation.get(); + Require(entered && onReleaseThread, "retired import destruction did not run on the recorder release thread"); + Require(reachedWaiter && blocked && !premature && registered, "guest mutation passed a retired import whose Vulkan free was still running"); + Require(applied.load(), "guest mutation did not resume after deferred Vulkan free"); + requireFreed(pending.imported); + pending.VerifyCopy(); +} + +void idleUnmap(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + allocation.Release(); + }); + allocation.registered = false; + } + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "an unmapped range still has a live host import"); +} + +void pendingRead(Fixture& fixture, bool submitted, bool locked) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::atomic completed{false}; + struct Finish { + Fixture& fixture; + ~Finish() { fixture.Cleanup(); } + } finish{fixture}; + std::unique_lock gpu(GpuMutex()); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation.Address(), allocation.bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Keep(std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + fixture.recorder->OnComplete([&] { completed.store(true); }); + if (submitted) fixture.recorder->Submit(); + if (!locked) gpu.unlock(); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + Require(completed.load(), "guest unmap passed a pending GPU read"); + result.Invalidate(); + const auto copied = result.Bytes(); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x37}; }), "GPU read lost the source contents before guest unmap"); + allocation.Release(); + }); + allocation.registered = false; + } +} + +void productionWaiterFailure(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + bool completed = false; + struct Finish { + Fixture& fixture; + ~Finish() { fixture.Cleanup(); } + } finish{fixture}; + const auto recordCopy = [&] { + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation.Address(), allocation.bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Keep(std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + }; + { + std::lock_guard gpu(GpuMutex()); + recordCopy(); + } + const auto rejectedBefore = fixture.calls.rejectedSubmissions.load(); + const auto acceptedBefore = fixture.calls.acceptedSubmissions.load(); + fixture.calls.failQueueSubmit.store(true); + bool applied = false; + std::string failure; + { + GuestAllocations::Mutation mutation; + try { + mutation.Protect(allocation.data, allocation.bytes, true, true, true, [&] { applied = true; }); + } catch (const std::runtime_error& error) { + failure = error.what(); + } + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "failed production waiter changed the guest registration"); + } + Require(!fixture.calls.failQueueSubmit.load() && fixture.calls.rejectedSubmissions.load() == rejectedBefore + 1, "production pin waiter did not reach the injected queue submission failure"); + Require(fixture.calls.acceptedSubmissions.load() == acceptedBefore, "failed production waiter accepted GPU work before reporting failure"); + Require(failure == "AGC graphics: vkQueueSubmit recorder: Vulkan result -1" && !applied, "production pin waiter did not propagate the original Vulkan error before host apply"); + Require(!imported->destroyed.load() && !imported->freed.load(), "failed production waiter retired its host import"); + Require(HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "failed production waiter removed the live import"); + { + std::lock_guard gpu(GpuMutex()); + recordCopy(); + fixture.recorder->OnComplete([&] { completed = true; }); + } + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + Require(completed, "production waiter retry passed a pending GPU read"); + result.Invalidate(); + const auto copied = result.Bytes(); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x37}; }), "production waiter retry lost its real GPU copy"); + allocation.Release(); + }); + allocation.registered = false; + } + Require(fixture.calls.acceptedSubmissions.load() == acceptedBefore + 1, "production waiter retry did not submit exactly its new copy"); + requireFreed(imported); +} + +void partialUnmap(Fixture& fixture) { + Allocation allocation(fixture, 3); + Allocation unrelated(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + const auto other = fixture.Import(unrelated.Address(), unrelated.bytes); + auto* middle = static_cast(allocation.data) + allocation.alignment; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(middle, allocation.alignment, [&](const void* piece, std::size_t bytes, const void* original, bool last) { + requireFreed(imported); + Require(!other->freed.load(), "partial unmap retired a disjoint host import"); + Require(piece == middle && bytes == allocation.alignment && original == allocation.data && !last, "partial unmap changed the wrong guest extent"); + }); + } + Require(!RegisteredReadableCovers(reinterpret_cast(middle), allocation.alignment), "the middle of a partial unmap remains registered"); + const auto low = fixture.Import(allocation.Address(), allocation.alignment); + const auto high = fixture.Import(allocation.Address() + 2 * allocation.alignment, allocation.alignment); + Require(!low->freed.load() && !high->freed.load() && !other->freed.load(), "remaining partial mappings could not be imported"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(low); + requireFreed(high); + Require(!other->freed.load(), "removing a split allocation retired a disjoint import"); +} + +void protectRollback(Fixture& fixture) { + Allocation allocation(fixture); + auto imported = fixture.Import(allocation.Address(), allocation.bytes); + bool called = false; + try { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, true, false, true, [&] { + requireFreed(imported); + called = true; + throw std::runtime_error("expected host protection failure"); + }); + throw std::runtime_error("host protection failure was ignored"); + } catch (const std::runtime_error& error) { + Require(called && std::string(error.what()) == "expected host protection failure", "unexpected protection failure"); + } + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "failed host protection changed the guest registration"); + } + imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, true, false, true, [&] { requireFreed(imported); }); + } + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && !range.writable && range.gpu, "successful protection did not update the guest registration"); + mutation.Protect(allocation.data, allocation.bytes, true, true, true, [] {}); + } + imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + +void blockedFree(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + imported->blockFree = true; + std::atomic applied{false}; + auto mutation = std::async(std::launch::async, [&] { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + applied.store(true); + allocation.Release(); + }); + allocation.registered = false; + }); + bool entered = false; + { + std::unique_lock lock(imported->mutex); + entered = imported->wake.wait_for(lock, std::chrono::seconds(2), [&] { return imported->freeing; }); + } + const bool premature = applied.load(); + const bool stillRegistered = RegisteredReadableCovers(allocation.Address(), allocation.bytes); + { + std::lock_guard lock(imported->mutex); + imported->allowFree = true; + } + imported->wake.notify_all(); + mutation.get(); + Require(entered, "guest unmap never reached Vulkan import destruction"); + Require(!premature && stillRegistered, "guest unmap released its registration while vkFreeMemory was blocked"); + Require(applied.load(), "guest unmap did not resume after vkFreeMemory returned"); +} + +void failedConstruction(Fixture& fixture) { + Allocation allocation(fixture); + fixture.calls.failAddressLookup.store(true); + bool failed = false; + try { + fixture.Import(allocation.Address(), allocation.bytes); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()).find("vkGetBufferDeviceAddressKHR") != std::string::npos; + } + Require(failed && !fixture.calls.failAddressLookup.load(), "import construction did not reach the injected Vulkan resolver failure"); + const auto allocated = fixture.calls.lastAllocated; + Require(allocated != nullptr && allocated->address == allocation.Address(), "resolver failure happened before host memory was imported"); + if (!allocated->freed.load()) { + destroyBuffer(fixture.context.device, allocated->buffer, nullptr); + freeMemory(fixture.context.device, allocated->memory, nullptr); + throw std::runtime_error("failed host import construction leaked its Vulkan memory"); + } + requireFreed(allocated); + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "failed host import construction remained cached"); + const auto retried = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(retried); + Require(allocated->frees.load() == 1, "retry destroyed the failed import a second time"); +} + +void mutationDuringCompletion(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + bool rejected = false; + bool applied = false; + bool laterCompletion = false; + bool copied = false; + std::lock_guard gpu(GpuMutex()); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->OnComplete([&] { + try { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, false, false, false, [&] { applied = true; }); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("active GPU completion") != std::string::npos; + } + }); + fixture.recorder->OnComplete([&] { + laterCompletion = true; + result.Invalidate(); + const auto bytes = result.Bytes(); + copied = std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0x37}; }); + }); + fixture.recorder->Sync(); + Require(rejected && !applied, "an active completion allowed guest memory mutation before later completions"); + Require(laterCompletion && copied, "rejecting a completion mutation lost later GPU results"); + Require(!imported->freed.load(), "rejecting a completion mutation retired its host import"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "rejecting a completion mutation changed its guest range"); + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + +void completedLeaseUnderLock(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + std::lock_guard gpu(GpuMutex()); + fixture.recorder->Keep(std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + fixture.recorder->Sync(); + Require(fixture.recorder->Idle(), "completed lease test recorder is not idle"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + + +struct MutationRace { + Fixture* fixture; + Allocation* allocation; + std::shared_ptr replacement; + unsigned calls = 0; +}; + +MutationRace* mutationRace = nullptr; + +bool raceWaiter(std::uintptr_t address, std::size_t bytes) { + auto& race = *mutationRace; + Require(address == race.allocation->Address() && bytes == race.allocation->bytes, "pin waiter received the wrong mutation extent"); + std::lock_guard gpu(GpuMutex()); + race.fixture->Cleanup(); + if (++race.calls == 1) race.replacement = race.fixture->Import(address, bytes); + return true; +} + +bool failingWaiter(std::uintptr_t, std::size_t) { + throw std::runtime_error("expected pin waiter failure"); +} + +void waiterFailure(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&failingWaiter); + bool failed = false; + bool excluded = false; + std::future reader; + { + GuestAllocations::Mutation mutation; + try { + mutation.Protect(allocation.data, allocation.bytes, false, false, false, [] { throw std::runtime_error("host apply ran after a pin waiter failure"); }); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()) == "expected pin waiter failure"; + } + std::promise starting; + auto ready = starting.get_future(); + reader = std::async(std::launch::async, [&] { + starting.set_value(); + static_cast(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + }); + ready.wait(); + excluded = reader.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "a pin waiter failure changed the registered permissions"); + } + reader.get(); + Require(failed, "a pin waiter error did not propagate"); + Require(excluded, "a failed pin waiter did not restore the registry lock"); + Require(!imported->freed.load(), "a failed pin waiter retired an import"); +} + +void reimportBeforeRelock(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + MutationRace race{&fixture, &allocation}; + mutationRace = ∽̱ + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&raceWaiter); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + Require(race.calls == 2 && race.replacement != nullptr, "guest mutation did not recheck an import acquired before registry relock"); + requireFreed(imported); + requireFreed(race.replacement); + allocation.Release(); + }); + allocation.registered = false; + } + mutationRace = nullptr; +} + +} + +int RunHostImportLifetimeTests(const Context& context) { + if (context.hostImportAlignment == 0) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return 77; + } + Fixture fixture(context); + descriptorFirstImport(fixture); + idleUnmap(fixture); + pendingRead(fixture, false, false); + pendingRead(fixture, true, false); + pendingRead(fixture, false, true); + pendingRead(fixture, true, true); + partialUnmap(fixture); + protectRollback(fixture); + completedLeaseUnderLock(fixture); + mutationDuringCompletion(fixture); + blockedFree(fixture); + failedConstruction(fixture); + productionWaiterFailure(fixture); + deferredRetirement(fixture); + std::puts("host import lifetime tests passed"); + return 0; +} + +int RunHostImportMutationRaceTests(const Context& context) { + if (context.hostImportAlignment == 0) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return 77; + } + Fixture fixture(context); + waiterFailure(fixture); + reimportBeforeRelock(fixture); + std::puts("host import mutation race tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp new file mode 100644 index 0000000000..9e28ea9da5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTLIFETIME_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTLIFETIME_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +int RunHostImportLifetimeTests(const AgcDriver::Graphics::Context& context); + +int RunHostImportMutationRaceTests(const AgcDriver::Graphics::Context& context); + +#endif 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 de92d1672f..45c02ae30e 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; @@ -51,22 +52,39 @@ struct Bind { struct MockDevice { std::uintptr_t next = 0x1000; std::set buffers; + std::set fences; std::set sparse; std::map sizes; std::map memories; std::vector binds; VkResult bindResult = VK_SUCCESS; std::uint64_t nextAddress = 0x500000000ull; + VkDeviceSize sparseAlignment = Alignment; }; MockDevice mock; VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = reinterpret_cast(mock.next++); + mock.fences.insert(*fence); return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {} +VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) { + mock.fences.erase(fence); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { + *commands = reinterpret_cast(mock.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t, const VkFence*) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockResetCommandBuffer(VkCommandBuffer, VkCommandBufferResetFlags) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { return VK_SUCCESS; } +VKAPI_ATTR void VKAPI_CALL mockCmdCopyBuffer(VkCommandBuffer, VkBuffer, VkBuffer, std::uint32_t, const VkBufferCopy*) {} VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { return VK_SUCCESS; @@ -94,8 +112,9 @@ VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const Vk } VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { - requirements->size = (mock.sizes.at(buffer) + Alignment - 1) / Alignment * Alignment; - requirements->alignment = Alignment; + const auto alignment = mock.sparse.contains(buffer) ? mock.sparseAlignment : Alignment; + requirements->size = (mock.sizes.at(buffer) + alignment - 1) / alignment * alignment; + requirements->alignment = alignment; requirements->memoryTypeBits = 1; } @@ -132,6 +151,13 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { static const std::map table{ + {"vkAllocateCommandBuffers", reinterpret_cast(mockAllocateCommandBuffers)}, + {"vkResetFences", reinterpret_cast(mockResetFences)}, + {"vkResetCommandBuffer", reinterpret_cast(mockResetCommandBuffer)}, + {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, + {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, + {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, + {"vkCmdCopyBuffer", reinterpret_cast(mockCmdCopyBuffer)}, {"vkCreateFence", reinterpret_cast(mockCreateFence)}, {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, @@ -227,6 +253,36 @@ std::uint64_t RangeBytes() { return (UdmabufLimitBytes() / ChunkBytes + 1) * ChunkBytes + (std::uint64_t{1} << 20u); } +void SettlingSurvivesDeviceReplacement(bool reverse) { + const auto baselineBuffers = mock.buffers.size(); + const auto baselineMemories = mock.memories.size(); + const auto baselineFences = mock.fences.size(); + SharedRange range(8u << 20u, 0, true); + auto first = mockContext(); + first.device = reinterpret_cast(mock.next++); + first.settleQueue = reinterpret_cast(mock.next++); + first.settlePool = reinterpret_cast(mock.next++); + std::mutex queueMutex; + first.settleQueueMutex = &queueMutex; + auto second = first; + second.device = reinterpret_cast(mock.next++); + second.settlePool = reinterpret_cast(mock.next++); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const auto& context : {first, second, first}) { + const auto* imported = HostImportFor(context, range.Address(), 4096); + Expect(imported != nullptr && imported->dmaBuf, "replacement device did not settle its dma-buf import"); + } + ClearHostImports(reverse ? second.device : first.device); + Expect(mock.fences.size() == baselineFences + 1, "clearing one device did not retain exactly the other device's settle fence"); + ClearHostImports(reverse ? first.device : second.device); + Expect(mock.buffers.size() == baselineBuffers, "device replacement leaked a settle buffer"); + Expect(mock.memories.size() == baselineMemories, "device replacement leaked settle memory"); + Expect(mock.fences.size() == baselineFences, "device replacement leaked a settle fence"); + ClearHostImports(first.device); + ClearHostImports(second.device); + Expect(mock.fences.size() == baselineFences, "repeated device cleanup changed the live fences"); +} + void ImportsALargeRangeAsOneSparseBuffer() { const auto bytes = RangeBytes(); SharedRange range(bytes, 0, true); @@ -251,14 +307,35 @@ void ImportsALargeRangeAsOneSparseBuffer() { Expect(memories.size() == chunks, "two parts of the range are bound to the same chunk import"); } -void UnalignedOffsetIsDecidedBeforeImporting() { +void UnalignedOffsetStartsAtTheBlockBelow() { const auto bytes = RangeBytes(); - SharedRange range(bytes, 4096, true); + constexpr std::uint64_t lead = 4096; + SharedRange range(bytes, lead, true); const auto context = mockContext(); mock.binds.clear(); + const auto address = mock.nextAddress; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import != nullptr, "a range whose backing offset is not 64 KiB aligned was not imported above udmabuf's size limit"); + if (import == nullptr) return; + Expect(mock.sparse.contains(import->buffer) && import->lead == lead, "the import does not start its sparse buffer at the 64 KiB block below the range"); + Expect(import->address == address + lead && HostImportOffset(*import, range.Address()) == lead, "the import's device address or buffer offset does not point at the range itself"); + Expect(!mock.binds.empty() && mock.binds.front().resourceOffset == 0 && mock.binds.front().memoryOffset == 0, "the first bind does not map the block below the range"); + VkDeviceSize bound = 0; + for (const auto& bind : mock.binds) bound += bind.size; + Expect(bound >= lead + bytes, "the binds do not cover the range after its lead"); +} + +void LargerSparseBlockIsDecidedBeforeImporting() { + const auto bytes = RangeBytes(); + SharedRange range(bytes, Alignment, true); + const auto context = mockContext(); + mock.binds.clear(); + mock.sparseAlignment = 2 * Alignment; const auto memories = mock.memories.size(); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + mock.sparseAlignment = Alignment; Expect(import == nullptr, "a range whose backing offset is not on the sparse block size was imported above udmabuf's size limit"); Expect(mock.memories.size() == memories && mock.binds.empty(), "a chunk was imported or bound for a range that does not take the sparse path"); } @@ -290,11 +367,19 @@ 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 { + SettlingSurvivesDeviceReplacement(false); + SettlingSurvivesDeviceReplacement(true); ImportsALargeRangeAsOneSparseBuffer(); - UnalignedOffsetIsDecidedBeforeImporting(); + UnalignedOffsetStartsAtTheBlockBelow(); + LargerSparseBlockIsDecidedBeforeImporting(); FailureAfterTheDecisionThrows(false, VK_SUCCESS, "UDMABUF_CREATE"); FailureAfterTheDecisionThrows(true, VK_ERROR_DEVICE_LOST, "vkQueueBindSparse"); } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp new file mode 100644 index 0000000000..8901e04a35 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -0,0 +1,115 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t workgroupBytes(std::span words) { + std::map> types; + std::map constants; + std::vector pointers; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); + const auto instruction = words.subspan(offset, count); + const auto opcode = static_cast(instruction[0] & 0xffffu); + if (opcode == spv::OpTypeInt || opcode == spv::OpTypeFloat || opcode == spv::OpTypeVector || opcode == spv::OpTypeArray || opcode == spv::OpTypeStruct || opcode == spv::OpTypePointer) types.emplace(instruction[1], instruction); + if (opcode == spv::OpConstant) constants.emplace(instruction[2], instruction[3]); + if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]); + offset += count; + } + const std::function size = [&](std::uint32_t id) -> std::uint64_t { + const auto type = types.at(id); + switch (static_cast(type[0] & 0xffffu)) { + case spv::OpTypeInt: case spv::OpTypeFloat: return type[2] / 8u; + case spv::OpTypeVector: return size(type[2]) * type[3]; + case spv::OpTypeArray: return size(type[2]) * constants.at(type[3]); + case spv::OpTypeStruct: { + std::uint64_t bytes = 0; + for (const auto member : type.subspan(2)) bytes += size(member); + return bytes; + } + default: throw std::runtime_error("unexpected Workgroup variable type"); + } + }; + std::uint64_t bytes = 0; + for (const auto pointer : pointers) { + const auto type = types.at(pointer); + require((type[0] & 0xffffu) == spv::OpTypePointer, "Workgroup variable is not a pointer"); + bytes += size(type[3]); + } + return bytes; +} + +RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { + static constexpr std::array code{ + 0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u, + 0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u + }; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + static constexpr std::array userData{}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 16u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = limit; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + return Recompile(request); +} + +void verify(std::uint32_t waveSize, std::uint32_t limit) { + const auto result = compile(waveSize, limit); + require(!result.spirv.empty(), "LDS atomic program did not compile"); + const auto declared = workgroupBytes(result.spirv.Words()); + require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit)); + require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); + if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); +} + +} + +int main() { + unsigned failures = 0; + for (const auto waveSize : {32u, 64u}) { + for (const auto limit : {64u, 68u, 72u}) { + const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit); + try { + verify(waveSize, limit); + std::cout << "LDS lock budget: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + const std::string message(error.what()); + std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + } + } + } + if (failures != 0) return 1; + std::cout << "LDS lock budget tests passed\n"; + return 0; +} 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..7c4bd1b701 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -28,6 +28,8 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "ResidentPresent.hpp" +#include "ImageTableAbi.hpp" +#include "HostImportLifetime.hpp" #include "SampleLod_spv.h" #include "BufferUpdate_spv.h" #include "SampleArray_spv.h" @@ -51,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +111,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 +193,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 +257,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}; @@ -264,6 +313,28 @@ public: extensionsEnabled.insert(extensionsEnabled.end(), libraryExtensions.begin(), libraryExtensions.end()); address.pNext = &library; } + VkPhysicalDeviceDescriptorIndexingFeaturesEXT indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; + if (hasExtension(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &indexing}; + function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); + context.descriptorIndexing = indexing.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && indexing.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; + context.partiallyBound = indexing.descriptorBindingPartiallyBound == VK_TRUE; + context.variableDescriptorCount = indexing.descriptorBindingVariableDescriptorCount == VK_TRUE; + context.runtimeDescriptorArray = indexing.runtimeDescriptorArray == VK_TRUE; + } + indexing = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; + if (context.descriptorIndexing && context.partiallyBound && context.variableDescriptorCount && context.runtimeDescriptorArray) { + extensionsEnabled.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + indexing.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; + indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; + indexing.descriptorBindingPartiallyBound = VK_TRUE; + indexing.descriptorBindingVariableDescriptorCount = VK_TRUE; + indexing.runtimeDescriptorArray = VK_TRUE; + indexing.pNext = address.pNext; + address.pNext = &indexing; + } else { + context.descriptorIndexing = context.partiallyBound = context.variableDescriptorCount = context.runtimeDescriptorArray = false; + } VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness}; @@ -292,6 +363,7 @@ public: std::cout << "Vulkan device: " << properties.deviceName << '\n'; context.limits = properties.limits; context.bufferDeviceAddress = true; + context.imageTableCapacity = ImageTableCapacity(context); context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); @@ -1534,6 +1606,374 @@ void storeRunTests(const Device& device, Recorder& recorder) { HostImportFor(context, address, bytes); } +#ifdef _WIN32 +bool deferredSettleTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::size_t settledBytes = std::size_t{1} << 20; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: deferred settles not tested\n"; + return false; + } + const auto registerRange = [](void* block, std::size_t bytes) { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + }; + const auto unregisterRange = [](void* block) { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + }; + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + void* large = AllocateWatched(settledBytes, unit); + void* spacer = AllocateWatched(unit, unit); + void* small = AllocateWatched(unit, unit); + struct Release { + void* large; + void* spacer; + void* small; + ~Release() { + ReleaseWatched(large, settledBytes); + ReleaseWatched(spacer, unit); + ReleaseWatched(small, unit); + } + } release{large, spacer, small}; + const auto largeAddress = reinterpret_cast(large); + Require(reinterpret_cast(small) != largeAddress + settledBytes && reinterpret_cast(small) + unit != largeAddress, "(d) the 64 KiB block is next to the 1 MiB block, so their imports would join"); + registerRange(large, settledBytes); + const auto* settled = HostImportFor(context, largeAddress, settledBytes); + Require(settled != nullptr, "(d) the 1 MiB import was refused"); + if (settled->unwatched) { + std::cout << "this device compares every Windows import: deferred settles not tested\n"; + unregisterRange(large); + HostImportFor(context, largeAddress, settledBytes); + return false; + } + const auto address = reinterpret_cast(small); + std::memset(small, 0x11, unit); + registerRange(small, unit); + const auto poll = [&](const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!done() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + return done(); + }; + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched && !Watched(address, unit), "(d) a 64 KiB import was not compared at first"); + Require(poll([&] { return Watched(address, unit); }), "(d) a 64 KiB import did not return to the write watch after its settle"); + const auto* back = HostImportFor(context, address, unit); + Require(back == import && !back->unwatched, "(d) the import back in the write watch is still marked compared"); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, unit); + Require(collected != 0, "(d) the import back in the write watch is not collected"); + static_cast(small)[4096] = 0x22; + CollectWritesUncached(address, unit); + Require(!UnchangedSince(address, unit, collected), "(d) a CPU store after the import returned to the write watch was missed"); + gpu.unlock(); + const auto beforeRounds = HostImportSettles(); + for (int round = 0; round < 64; ++round) { + unregisterRange(small); + registerRange(small, unit); + const auto* again = HostImportFor(context, address, unit); + Require(again != nullptr && again->unwatched && !Watched(address, unit), "(d) a 64 KiB buffer mapped again off the device lock was not imported compared"); + } + const auto afterRounds = HostImportSettles(); + Require(afterRounds.submitted == beforeRounds.submitted && afterRounds.waited == beforeRounds.waited, "(d) importing 64 KiB buffers off the device lock submitted or waited for a settle"); + gpu.lock(); + const auto beforeLocked = HostImportSettles(); + for (int round = 0; round < 64; ++round) { + unregisterRange(small); + registerRange(small, unit); + const auto* again = HostImportFor(context, address, unit); + Require(again != nullptr && again->unwatched && !Watched(address, unit), "(d) a 64 KiB buffer mapped again under the device lock was not imported compared"); + } + const auto afterLocked = HostImportSettles(); + Require(afterLocked.submitted == beforeLocked.submitted && afterLocked.waited == beforeLocked.waited, "(d) mapping a 64 KiB buffer again under the device lock, with no lookup between, submitted or waited for a settle"); + Require(poll([&] { return Watched(address, unit); }), "(d) the last buffer mapped again did not return to the write watch"); + unregisterRange(small); + registerRange(small, unit); + Require(HostImportFor(context, address, unit) != nullptr, "(d) a buffer mapped again after it returned to the write watch was not imported"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + unregisterRange(small); + registerRange(small, unit); + const auto* replaced = HostImportFor(context, address, unit); + Require(replaced != nullptr && replaced->unwatched, "(d) a buffer mapped again while its settle may be in flight was not imported"); + Require(poll([&] { return Watched(address, unit); }), "(d) the buffer mapped again while its settle was in flight did not return to the write watch"); + unregisterRange(large); + registerRange(large, unit); + const auto* piece = HostImportFor(context, largeAddress, unit); + Require(piece != nullptr && piece->unwatched, "(d) the first 64 KiB of the large block was not imported compared"); + unregisterRange(large); + registerRange(large, settledBytes); + const auto* whole = HostImportFor(context, largeAddress, settledBytes); + Require(whole != nullptr && whole->bytes == settledBytes, "(d) the large block mapped whole again was not imported whole"); + Require(poll([&] { return Watched(largeAddress, settledBytes); }), "(d) a settled import over a piece that had left the write watch did not return to it"); + SetHostImportDeferredSettles(false); + unregisterRange(small); + registerRange(small, unit); + const auto* kept = HostImportFor(context, address, unit); + Require(kept != nullptr && kept->unwatched, "(d) with deferred settles off, a 64 KiB buffer mapped again was not imported compared"); + for (int round = 0; round < 20; ++round) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + const bool stayed = !Watched(address, unit); + SetHostImportDeferredSettles(true); + Require(stayed, "(d) with deferred settles off, a 64 KiB import returned to the write watch"); + unregisterRange(small); + unregisterRange(large); + HostImportFor(context, address, unit); + std::cout << "deferred settle tests passed\n"; + return true; +} + +bool settledImportsWatchPrivate(const Context& context) { + using namespace AgcDriver::GuestMemory; + constexpr std::size_t settledBytes = std::size_t{1} << 20; + void* large = AllocateWatched(settledBytes, 65536); + const auto address = reinterpret_cast(large); + { + GuestAllocations::Mutation mutation; + mutation.Add(large, settledBytes, true, true, true); + } + const auto* settled = HostImportFor(context, address, settledBytes); + const bool watched = settled != nullptr && !settled->unwatched; + { + GuestAllocations::Mutation mutation; + mutation.Remove(large); + } + HostImportFor(context, address, settledBytes); + ReleaseWatched(large, settledBytes); + Require(settled != nullptr, "the 1 MiB import was refused"); + return watched; +} + +bool settleFirstUseRaced(const Context& context, int copies) { + using namespace AgcDriver::GuestMemory; + constexpr std::size_t unit = 65536; + void* small = AllocateWatched(unit, unit); + struct Release { + void* small; + ~Release() { ReleaseWatched(small, unit); } + } release{small}; + const auto address = reinterpret_cast(small); + std::memset(small, 0x11, unit); + Recorder recorder(context); + recorder.Activate(); + Buffer scratch(context, unit, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + GuestAllocations::Mutation mutation; + mutation.Add(small, unit, true, true, true); + } + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched && !Watched(address, unit), "(f) a 64 KiB import was not compared at first"); + const auto commands = recorder.Commands(); + for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + recorder.Submit(); + const auto submittedAt = std::chrono::steady_clock::now(); + const auto deadline = submittedAt + std::chrono::seconds(5); + std::uint64_t collected = 0; + bool returned = false; + auto returnedAt = submittedAt; + while (!returned && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + if (Watched(address, unit)) { + returnedAt = std::chrono::steady_clock::now(); + BumpCollectEpoch(); + collected = CollectWrites(address, unit); + returned = true; + } + } + recorder.Reap(); + const bool racing = returned && recorder.InFlightBatches() != 0; + while (recorder.InFlightBatches() != 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::microseconds(200)); + recorder.Reap(); + } + const auto reapedAt = std::chrono::steady_clock::now(); + recorder.Sync(); + CollectWritesUncached(address, unit); + const bool unchanged = returned && UnchangedSince(address, unit, collected); + const auto since = [&](std::chrono::steady_clock::time_point at) { return std::chrono::duration(at - submittedAt).count(); }; + std::cout << "(f) settled import back in the write watch " << since(returnedAt) << " ms after " << copies << " main-queue copies of it were submitted, copies reaped by " << since(reapedAt) << " ms; driver marks after its return=" << !unchanged << '\n'; + Require(returned, "(f) a settled import did not return to the write watch"); + Require(unchanged, "(f) the driver marked a settled import's pages after its settle read completed while earlier main-queue copies of it ran"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(small); + } + HostImportFor(context, address, unit); + return racing; +} + +bool settleFirstUseTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: the first use of a settled import not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; + return false; + } + for (int copies = 4096; copies <= 65536; copies *= 4) { + if (settleFirstUseRaced(context, copies)) { + std::cout << "settled import first use tests passed\n"; + return true; + } + std::cout << "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them\n"; + } + std::cout << "(f) the main-queue copies finished first in every round: the first use of a settled import not tested\n"; + return false; +} + +bool settleEarlierImportTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr int copies = 4096; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: an earlier import's first use after a settled import's return not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: an earlier import's first use after a settled import's return not tested\n"; + return false; + } + void* pair = AllocateWatched(2 * unit, unit); + struct Release { + void* pair; + ~Release() { + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ReleaseWatched(pair, 2 * unit); + } + } release{pair}; + const auto address = reinterpret_cast(pair); + std::memset(pair, 0x11, 2 * unit); + Recorder recorder(context); + recorder.Activate(); + Buffer scratch(context, unit, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + SetHostImportRangeWindow(unit); + { + GuestAllocations::Mutation mutation; + mutation.Add(pair, 2 * unit, true, true, true); + } + const auto* earlier = HostImportFor(context, address, 16); + Require(earlier != nullptr && earlier->base == address && earlier->bytes == unit && earlier->unwatched, "(e) the first window of the range was not imported compared"); + const auto commands = recorder.Commands(); + for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + const auto* whole = HostImportFor(context, address + unit - 16, 32); + Require(whole != nullptr && whole->base == address && whole->bytes == 2 * unit && whole->unwatched, "(e) an access across the windows did not replace the first window's import"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!Watched(address, 2 * unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address + unit - 16, 32); + } + Require(Watched(address, 2 * unit), "(e) the range's import did not return to the write watch"); + Require(recorder.Recording(), "(e) the earlier import's copies were submitted before the range's import returned to the write watch"); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, 2 * unit); + const auto submittedAt = std::chrono::steady_clock::now(); + recorder.Sync(); + const auto syncedAt = std::chrono::steady_clock::now(); + CollectWritesUncached(address, 2 * unit); + const bool unchanged = UnchangedSince(address, 2 * unit, collected); + std::cout << "(e) the range's import back in the write watch before the replaced window's first use ran (" << std::chrono::duration(syncedAt - submittedAt).count() << " ms of copies after it); driver marks after its return=" << !unchanged << '\n'; + Require(unchanged, "(e) the driver marked the pages of a settled import while a replaced import of them ran its first use"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(pair); + } + HostImportFor(context, address, 16); + std::cout << "earlier import first use tests passed\n"; + return true; +} + +bool settleRetireTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr int rounds = 64; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: imports retired during their settle read not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: imports retired during their settle read not tested\n"; + return false; + } + void* pair = AllocateWatched(2 * unit, unit); + struct Release { + void* pair; + ~Release() { + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ReleaseWatched(pair, 2 * unit); + } + } release{pair}; + const auto address = reinterpret_cast(pair); + std::memset(pair, 0x11, 2 * unit); + const auto registerPair = [&] { + GuestAllocations::Mutation mutation; + mutation.Add(pair, 2 * unit, true, true, true); + }; + const auto unregisterPair = [&] { + GuestAllocations::Mutation mutation; + mutation.Remove(pair); + }; + Recorder recorder(context); + recorder.Activate(); + SetHostImportRangeWindow(unit); + registerPair(); + const auto before = HostImportSettles(); + for (int round = 0; round < rounds; ++round) { + const auto* cell = HostImportFor(context, address, 16); + Require(cell != nullptr && cell->base == address && cell->bytes == unit && cell->unwatched, "(r) the first window of the range was not imported compared"); + std::this_thread::sleep_for(std::chrono::microseconds(150)); + HostImportFor(context, address, 16); + const auto* grown = HostImportFor(context, address + unit - 16, 32); + Require(grown != nullptr && grown->base == address && grown->bytes == 2 * unit, "(r) an access across the windows did not replace the first window's import"); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + HostImportFor(context, address + unit - 16, 32); + recorder.Sync(); + recorder.FlushDeferredReleases(); + unregisterPair(); + registerPair(); + } + const auto after = HostImportSettles(); + const auto* last = HostImportFor(context, address + unit - 16, 32); + Require(last != nullptr && last->bytes == 2 * unit && last->unwatched, "(r) the range was not imported whole and compared after the rounds"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!Watched(address, 2 * unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address + unit - 16, 32); + } + const bool back = Watched(address, 2 * unit); + std::cout << "(r) " << rounds << " window imports replaced after their settle read was submitted: " << after.parked - before.parked << " while the read ran, " << after.waited - before.waited << " waits, " << after.unmapWaits - before.unmapWaits << " unmaps waited for a read\n"; + Require(after.waited == before.waited, "(r) replacing an import during its settle read waited for the read"); + Require(after.parked != before.parked, "(r) no import was replaced while its settle read ran"); + Require(back, "(r) the import that replaced the windows did not return to the write watch"); + recorder.Sync(); + recorder.FlushDeferredReleases(); + unregisterPair(); + HostImportFor(context, address, 16); + std::cout << "imports retired during their settle read tests passed\n"; + return true; +} +#endif + void remappedImportTests(const Device& device) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { @@ -1980,10 +2420,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 +2783,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { }; check(); guest[offset + 8] ^= 0xffu; + AgcDriver::GuestMemory::MarkWritten(view, elementBytes); check(); regionRecorder.Sync(); } @@ -2434,6 +2877,7 @@ public: struct Contents { std::map, VkDescriptorBufferInfo> buffers; std::map, VkDescriptorImageInfo> images; + std::map, std::uint32_t> written; }; explicit DescriptorUpdates(Context& context) : context(context), previous(context.functions), update(context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")) { @@ -2452,6 +2896,7 @@ private: Contents contents; for (std::uint32_t i = 0; i < count; ++i) { const auto& write = writes[i]; + contents.written[{write.dstSet, write.dstBinding}] += write.descriptorCount; for (std::uint32_t element = 0; element < write.descriptorCount; ++element) { const auto key = std::pair{write.dstBinding, write.dstArrayElement + element}; if (write.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) contents.buffers[key] = write.pBufferInfo[element]; @@ -2522,6 +2967,85 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) { recorder.Activate(); } +void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + const auto& base = device.GetContext(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + if (context.imageTableCapacity < 2u) { + std::cout << "image tables unavailable: draw snapshot table sets not tested\n"; + return; + } + constexpr std::uint32_t width = 4; + constexpr std::uint32_t height = 4; + alignas(256) static std::array texels{}; + texels.fill(1.0f); + const auto address = static_cast(reinterpret_cast(texels.data())); + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + const auto addTable = [&](ShaderRecompiler::RecompileResult& program, ShaderRecompiler::RuntimeAbi::Stage stage, std::uint32_t set, std::uint32_t elements) { + ShaderRecompiler::DescriptorBinding map{}; + map.kind = DescriptorKind::StorageBuffer; + map.role = DescriptorRole::ImageTableMap; + map.descriptorSet = set; + map.binding = static_cast(stage) * bindingCount + static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.assign(Abi::HeaderWords + elements, 0u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = elements; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + elements; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::ImageTable; + images.descriptorSet = set; + images.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + images.count = elements; + for (std::uint32_t element = 0; element < elements; ++element) images.guestDescriptor.insert(images.guestDescriptor.end(), texture.begin(), texture.end()); + images.imageShapes.assign(elements, ShaderRecompiler::DescriptorImageShape::Image2D); + images.imageDepthCompare.assign(elements, false); + images.imageSamplers.assign(elements, 0u); + program.bindings.push_back(std::move(images)); + }; + DescriptorUpdates updates(context); + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + Recorder snapshots(context); + snapshots.Activate(); + ShaderRecompiler::RecompileResult vertex, fragment; + ShaderRecompiler::DescriptorBinding data{}; + data.kind = DescriptorKind::StorageBuffer; + data.role = DescriptorRole::FlattenedSrt; + data.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::FlattenedSrt); + data.count = 1; + data.guestDescriptor = {1, 2, 3, 4}; + vertex.bindings.push_back(data); + addTable(vertex, ShaderRecompiler::RuntimeAbi::Stage::Main, 0u, 1u); + addTable(fragment, ShaderRecompiler::RuntimeAbi::Stage::Fragment, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 2u); + const ColorTarget target{}; + ShaderResources resources(context, vertex, fragment, target, 0, 0); + Require(resources.SplitSets(), "a fragment image table did not get its own descriptor set"); + const ShaderResources::MovedBuffer moved{0, 0, 16, {5, 6, 7, 8}}; + const auto draw = resources.PrepareDrawBindings(snapshots, std::span(&moved, 1)); + Require(draw != nullptr && draw->snapshots.size() == 1, "moved shader data did not create a draw snapshot"); + const auto& written = updates.calls.back().written; + const auto tableWrites = [&](VkDescriptorSet set) { + const auto it = written.find({set, ShaderRecompiler::RuntimeAbi::ImageTableBinding}); + return it == written.end() ? 0u : it->second; + }; + Require(tableWrites(draw->allocation.set) == 1u, "the draw snapshot wrote the wrong number of image table descriptors into the vertex set"); + Require(tableWrites(draw->fragmentAllocation.set) == 2u, "the draw snapshot wrote the wrong number of image table descriptors into the fragment set"); + snapshots.Sync(); + } + ClearCachedTextures(context.device); + recorder.Activate(); +} + void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); constexpr std::size_t bytes = 65536; @@ -3131,10 +3655,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 +4331,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 +5860,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 +5937,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 +5959,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 +6016,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 +6117,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 +6795,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 +6881,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 +7005,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 +7471,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 +7850,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 +7953,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 +8077,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 +8431,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 +8650,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"; @@ -7972,13 +8662,15 @@ int main(int argc, char** argv) { } std::unique_ptr created; try { - created = std::make_unique(argc == 2 && std::string_view(argv[1]) == "--import-watch-other-family-only"); + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; return 77; } Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-only") return RunHostImportLifetimeTests(device.GetContext()); + if (argc == 2 && std::string_view(argv[1]) == "--host-import-mutation-race-only") return RunHostImportMutationRaceTests(device.GetContext()); if (argc == 2 && std::string_view(argv[1]) == "--import-watch-only") { importWatchTests(device); return 0; @@ -7991,6 +8683,18 @@ int main(int argc, char** argv) { importWatchTests(device); return 0; } +#ifdef _WIN32 + if (argc == 2 && (std::string_view(argv[1]) == "--deferred-settle-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")) { + if (std::string_view(argv[1]) == "--deferred-settle-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return deferredSettleTests(device) ? 0 : 77; + } + if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; + if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; + if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; +#endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { const auto alignment = device.GetContext().hostImportAlignment; if (alignment == 0 || alignment > 65536) { @@ -8077,6 +8781,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; @@ -8199,6 +8907,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); + drawSnapshotTableSetTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } @@ -8256,6 +8965,7 @@ int main(int argc, char** argv) { misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); + drawSnapshotTableSetTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); unwrittenSnapshotTests(device); @@ -8294,6 +9004,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/RobustBufferAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp new file mode 100644 index 0000000000..6b0331109f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp @@ -0,0 +1,33 @@ +#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_AMD_PROPRIETARY, false}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, + {VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_MOLTENVK, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::RobustBufferAccess2Usable(item.driver) != item.usable) { + std::fprintf(stderr, "driver %d: expected robustBufferAccess2 to be %s\n", static_cast(item.driver), item.usable ? "usable" : "unusable"); + ++failures; + } + } + return failures == 0 ? 0 : 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 3c7743b54e..e8f3b6a3ce 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,8 @@ 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("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = 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 +734,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 +1197,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; @@ -1415,6 +1506,10 @@ std::vector planFixtures() { add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + auto keyed = tables; + keyed.resize(20u, 0u); + keyed[16] = 3u; + add("bindless selected record", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, keyed}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); std::vector phis(32, 0u); place(phis, 0, imageWords(PlanTextureAddress)); @@ -1560,6 +1655,8 @@ IrResourcePlan handBuiltPlan() { table.dwordCount = 8u; table.dwords[0] = &readConst(3); table.tableColumn = TableColumn{2u, 32u, 16u, 4u, 8u, 0xffu, false, true, {KeyDomain{3u, 4u, 16u, false}, KeyDomain{2u, 8u, 4u, true}}, false}; + table.tableColumn->selected = true; + table.tableColumn->selection = 2u; DescriptorSource heap; heap.dwordCount = 4u; heap.dwords[1] = &readConst(6); @@ -1808,6 +1905,7 @@ void verifyMalformedPlans() { rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->heapSource = 4u; }); rejects("an image table key past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->keyDomains[1].source = 4u; }); + rejects("an image table selection past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->selection = 4u; }); auto foreign = handBuiltPlan(); IrValue stray(IrOpcode::Void, IrType::U32, 99u); foreign.srtReads[0].value = &stray; @@ -1867,6 +1965,7 @@ void verifyPlanRoundTrip() { require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->keyDomains.empty(); }); }), "no fixture plans a whole image table"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && !source.tableColumn->keyDomains.empty(); }); }), "no fixture plans an image table with a key domain"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->address; }); }), "no fixture plans an image table through a pointer"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->selected; }); }), "no fixture plans a selected image table record"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi"); require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); @@ -2096,6 +2195,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(); @@ -2128,9 +2228,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 a3294a30fe..f55927e50d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -10,6 +10,7 @@ #include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker/SrtTape.hpp" #include "Optimization/SrtWalker.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +56,26 @@ namespace { +thread_local std::uint64_t allocationCount = 0; + +} + +void* operator new(std::size_t bytes) { + allocationCount++; + if (auto* allocation = std::malloc(bytes == 0 ? 1 : bytes)) return allocation; + throw std::bad_alloc(); +} + +void operator delete(void* allocation) noexcept { + std::free(allocation); +} + +void operator delete(void* allocation, std::size_t) noexcept { + std::free(allocation); +} + +namespace { + ShaderRecompiler::SpirvTarget BufferTarget() { static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; @@ -561,9 +583,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 +613,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 +726,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 +919,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 +965,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({}); @@ -944,6 +1002,135 @@ void verifyGuardedDescriptorSources() { expectFailure([&] { static_cast(compileStore(0u)); }, "incompatible with the static runtime image interface", "guarded images: a store of the wrong dimension behind a taken branch was accepted"); } +bool readDirect(void*, std::uint64_t address, std::uint32_t* value) { + std::memcpy(value, reinterpret_cast(address), sizeof(*value)); + return true; +} + +void verifySrtTape() { + using namespace ShaderRecompiler; + struct Walk { + bool ok = false; + std::vector results; + std::vector flat; + std::vector active; + std::size_t poisoned = 0; + std::uint32_t nullRoots = 0; + std::vector> leaves; + std::vector reads; + std::uint64_t asBase = 0; + std::uint64_t asValue = 0; + std::vector> userDataReads; + bool operator==(const Walk&) const = default; + }; + const auto walk = [](const IrResourcePlan& plan, SrtRuntime runtime, bool tape) { + Walk out; + SrtReadTrace trace; + SrtUserDataUse use; + runtime.readTrace = &trace; + runtime.userDataUse = &use; + const std::vector sources(plan.materializationSources.begin(), plan.materializationSources.end()); + const std::vector clean(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end()); + const auto sourceSpan = tape ? std::span(plan.materializationSources) : std::span(sources); + const auto cleanSpan = tape ? std::span(plan.cleanFlatSlots) : std::span(clean); + std::vector poison; + out.ok = Detail::EvaluateRuntimeSourcesImpl(plan, sourceSpan, runtime, out.results, out.flat, true, cleanSpan, out.active, &poison, &out.nullRoots); + out.poisoned = poison.size(); + out.leaves = trace.leaves; + out.reads = trace.otherReads; + std::sort(out.leaves.begin(), out.leaves.end()); + std::sort(out.reads.begin(), out.reads.end()); + out.reads.erase(std::unique(out.reads.begin(), out.reads.end()), out.reads.end()); + out.asBase = use.asBase; + out.asValue = use.asValue; + out.userDataReads = use.reads; + std::sort(out.userDataReads.begin(), out.userDataReads.end()); + out.userDataReads.erase(std::unique(out.userDataReads.begin(), out.userDataReads.end()), out.userDataReads.end()); + return out; + }; + const auto answers = [](const IrResourcePlan& plan, const SrtRuntime& runtime) { + const auto* tape = Detail::SrtTape::For(plan); + require(tape != nullptr, "SRT tape: a complete plan has no tape"); + std::vector results; + std::vector flat; + std::vector active; + return tape->Run(plan, runtime, results, flat, active); + }; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 0u, 0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array nullUserData{0u, 0u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const auto requestFor = [&](std::uint64_t address, const std::vector& code) { + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, address, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + return request; + }; + const auto runtimeFor = [&](std::span words) { + SrtRuntime runtime{words}; + runtime.readSpecializationMemory = readDirect; + return runtime; + }; + + const std::vector guarded{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + const auto guardedPlan = GetResourcePlan(requestFor(0x24000u, guarded)); + require(!guardedPlan->controlFlow.empty() && guardedPlan->info.images.size() == 1u, "SRT tape: the guarded program has no branch around its image"); + const auto imageActivity = guardedPlan->descriptorSources.size(); + for (const auto& [image, mode] : std::array*, std::uint32_t>, 4>{{{&invalid, 0u}, {&invalid, 1u}, {&valid, 0u}, {&valid, 1u}}}) { + srt.fill(0u); + std::copy(image->begin(), image->end(), srt.begin()); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; + const auto runtime = runtimeFor(userData); + require(answers(*guardedPlan, runtime), "SRT tape: the tape refused the guarded program's walk"); + const auto tape = walk(*guardedPlan, runtime, true); + require(tape == walk(*guardedPlan, runtime, false), "SRT tape: the tape's walk differs from the Evaluator's on the guarded program"); + require(tape.ok && tape.active.size() == imageActivity + 1u && tape.active[imageActivity] == mode, "SRT tape: the image behind the branch has the wrong activity"); + require(tape.asBase != 0u && tape.asValue != 0u && !tape.userDataReads.empty(), "SRT tape: the guarded program's walk reported no user-data use"); + } + + const auto nullRuntime = runtimeFor(nullUserData); + require(!answers(*guardedPlan, nullRuntime), "SRT tape: the tape answered a walk through a null user-data root"); + const auto nullWalk = walk(*guardedPlan, nullRuntime, true); + require(nullWalk == walk(*guardedPlan, nullRuntime, false) && nullWalk.ok && nullWalk.nullRoots != 0u, "SRT tape: a walk through a null user-data root differs from the Evaluator's"); + + std::vector outOfRange{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf4080a00u, 0xfa000080u, 0xbf8cc07fu, 0xf42c0814u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850008u}; + for (std::uint32_t dword = 0; dword < 8u; dword++) outOfRange.push_back(0xbe800300u | ((8u + dword) << 16u) | (32u + dword)); + outOfRange.insert(outOfRange.end(), {0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}); + srt.fill(0u); + std::copy(valid.begin(), valid.end(), srt.begin()); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + std::copy(valid.begin(), valid.end(), srt.begin() + 24); + const auto outOfRangePlan = GetResourcePlan(requestFor(0x25000u, outOfRange)); + const auto runtime = runtimeFor(userData); + require(answers(*outOfRangePlan, runtime), "SRT tape: the tape refused a constant read past its V#"); + require(walk(*outOfRangePlan, runtime, true) == walk(*outOfRangePlan, runtime, false), "SRT tape: a constant read past its V# differs from the Evaluator's"); +} + struct alignas(256) TableTexture { std::array bytes{}; }; @@ -1020,6 +1207,7 @@ struct TableGuest { std::array srt{}; }; +constexpr std::uint32_t TableFormat16UNorm = 7; constexpr std::uint32_t TableFormat8888UNorm = 56; constexpr std::uint32_t TableFormat32UInt = 20; constexpr std::uint32_t TableFormat16SInt = 12; @@ -1099,6 +1287,13 @@ TableCompiled CompileTable(ShaderRecompiler::RecompileRequest request) { return out; } +std::array TableSelected(const ShaderRecompiler::ImageTableSnapshot& tables, std::uint32_t table, const char* message) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + const auto& column = tables.tables.at(table); + require(column.records == 1u && column.keys == 1u && column.codes.size() == 1u && (column.codes[0] & Abi::PoisonFlag) == 0u, message); + return tables.words.at(column.codes[0]).dwords; +} + void verifyImageTableKeys() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -1230,6 +1425,102 @@ 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 verifyImageTableScratchReuse() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::uint32_t Entries = 3000; + static TableTexture textures[4]; + alignas(4096) static TableGuest guest; + alignas(4096) static std::array, Entries> heap{}; + const auto place = [&](std::uint32_t shift) { + for (std::uint32_t entry = 0; entry < Entries; entry++) heap[entry] = TableT(textures[entry % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, 1u + (entry + shift) % Entries); + }; + FillTableSrt(guest, heap.data(), 32u, Entries); + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableWholeCode, userData); + auto staticRequest = request; + const std::array absentResources{}; + staticRequest.context.userData = absentResources; + staticRequest.context.memory = {}; + auto staticProgram = PrepareResourceProgram(staticRequest); + ResourceMaterializer{}.ApplyStaticInterface(staticProgram); + const auto& info = staticProgram.Resources().info; + struct Resolved { + ImageTableSnapshot snapshot; + ResolvedImageTables tables; + std::uint64_t allocations = 0; + }; + const auto resolve = [&] { + CountingMemory memory; + SrtRuntime runtime; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + runtime.userContext = &memory; + runtime.readMemory = &CountingRead; + runtime.readSpecializationMemory = &CountingRead; + runtime.accessible = &CountingAccessible; + runtime.readWords = &CountingReadWords; + Resolved out; + const auto before = allocationCount; + const auto capture = CaptureResources(request, runtime); + auto tables = ResourceMaterializer::ResolveImageTables(info, capture->snapshot); + out.allocations = allocationCount - before; + out.snapshot = capture->snapshot.tables; + out.tables = std::move(tables); + return out; + }; + place(0u); + static_cast(resolve()); + place(7u); + const auto warm = resolve(); + const auto fresh = std::async(std::launch::async, resolve).get(); + require(warm.snapshot == fresh.snapshot && warm.tables.map == fresh.tables.map && warm.tables.faults == fresh.tables.faults && warm.tables.poison.size() == fresh.tables.poison.size(), "image table scratch: a snapshot resolved after another one on the same thread differs from one resolved on a new thread"); + require(warm.snapshot.words.size() == Entries && warm.tables.elements.size() == Entries && warm.tables.map[Abi::HeaderElementCount] == Entries, "image table scratch: the distinct records were not all indexed"); + const auto codes = TableCodes(warm.tables.map, 0); + for (std::uint32_t entry = 0; entry < Entries; entry++) { + const auto element = warm.tables.map.at(codes.at(entry) + Abi::ImageRecordElement); + require(warm.tables.elements.at(element).words == fresh.tables.elements.at(element).words && warm.tables.elements[element].words[2] == heap[entry][2], "image table scratch: a record resolved to another entry's element"); + } + require(warm.allocations < Entries / 4u, "image table scratch: snapshotting and resolving a table allocated per record"); +} + +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; @@ -1411,7 +1702,6 @@ void verifyImageTables() { void verifyImageTableKeyUnion() { using namespace ShaderRecompiler; - namespace Abi = ImageTableAbi; static TableTexture textures[3]; alignas(4096) static TableGuest guest; auto& heap = guest.heap; @@ -1426,24 +1716,25 @@ void verifyImageTableKeyUnion() { const auto userData = TableUserData(guest); const auto request = TableRequest(TableTwoKeyCode, userData); const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table key union: two keyed reads of one table were not planned as one image of one column"); - const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table key union: the column does not carry both value key domains"); + require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table key union: two keyed reads of one heap were not planned as two tables"); + const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn; + require(first.has_value() && second.has_value() && first->selected && second->selected && first->heapSource == second->heapSource && first->selection != second->selection, "image table key union: the two keys do not each select a record of the one heap"); + const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table key union: a table does not hold one readable selected record"); }; const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table key union: the snapshot does not hold the union of the two keys' records"); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table key union: the tables do not hold the two keys' records"); + require(TableCovered(compiled.regions, &heap[1], sizeof(heap[1])) && TableCovered(compiled.regions, &heap[3], sizeof(heap[3])) && !TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[2], sizeof(std::uint32_t)), "image table key union: records no key selects were captured"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); - require(images != nullptr && images->count == 3u, "image table key union: the binding does not hold the three distinct images of the selected records"); + require(images != nullptr && images->count == 2u, "image table key union: the binding does not hold the two distinct images of the selected records"); guest.srt[17] = 2u; const auto moved = CompileTable(request); - const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0); - require((movedSnapshot.codes[2] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[3] == outside && (movedSnapshot.codes[1] & Abi::PoisonFlag) == 0u && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move the union or changed the module"); + const auto& movedTables = moved.capture->snapshot.tables; + require(held(movedTables, 0u) == heap[1] && held(movedTables, 1u) == heap[2] && TableCovered(moved.regions, &heap[2], sizeof(heap[2])) && !TableCovered(moved.regions, &heap[3], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move its record alone or changed the module"); } void verifyImageTableLateKey() { using namespace ShaderRecompiler; - namespace Abi = ImageTableAbi; static TableTexture textures[3]; alignas(4096) static TableGuest guest; auto& heap = guest.heap; @@ -1458,15 +1749,16 @@ void verifyImageTableLateKey() { const auto userData = TableUserData(guest); const auto request = TableRequest(TableLateKeyCode, userData); const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table late key: two keyed reads of one table were not planned as one image of one column"); - const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table late key: the column does not carry both value key domains"); + require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table late key: two keyed reads of one heap were not planned as two tables"); + const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn; + require(first.has_value() && second.has_value() && first->selected && second->selected && first->selection != second->selection, "image table late key: a key read after the first image operation does not select its own record"); + const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table late key: a table does not hold one readable selected record"); }; const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table late key: the snapshot does not hold the union of the two keys' records"); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table late key: the tables do not hold the two keys' records"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); - require(images != nullptr && images->count == 3u, "image table late key: the binding does not hold the three distinct images of the selected records"); + require(images != nullptr && images->count == 2u, "image table late key: the binding does not hold the two distinct images of the selected records"); } void verifyImageTableUnkeyedAccess() { @@ -1489,13 +1781,19 @@ void verifyImageTableUnkeyedAccess() { const auto userData = TableUserData(guest); const auto request = TableRequest(*code, userData); const auto plan = GetResourcePlan(request); - require(!plan->info.images.empty() && std::all_of(plan->info.images.begin(), plan->info.images.end(), [](const ImageResource& image) { return image.table == 0u; }), "image table unkeyed access: the two reads were not planned as one table"); - const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && column->unkeyed && column->keyDomains.empty(), "image table unkeyed access: a column shared with a read that has no key domain kept a key domain"); + const auto& planned = plan->info.images; + require(planned.size() == 2u && std::all_of(planned.begin(), planned.end(), [](const ImageResource& image) { return image.table != NoTable; }), "image table unkeyed access: the two reads were not planned as two tables"); + const auto columnOf = [&](const ImageResource& image) { return *plan->descriptorSources.at(image.source).tableColumn; }; + const auto selected = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return columnOf(image).selected; }); + const auto whole = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return !columnOf(image).selected; }); + require(selected != planned.end() && whole != planned.end(), "image table unkeyed access: the read keyed through the SRT does not select its record beside the read keyed per lane"); + require(columnOf(*whole).unkeyed && columnOf(*whole).keyDomains.empty(), "image table unkeyed access: the read keyed per lane kept a key domain"); const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + const auto& tables = compiled.capture->snapshot.tables; + require(TableSelected(tables, selected->table, "image table unkeyed access: a table does not hold one readable selected record") == heap[2], "image table unkeyed access: the read keyed through the SRT does not hold its record"); + const auto& snapshot = tables.tables.at(whole->table); const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t code) { return code == outside; }), "image table unkeyed access: the shared column was narrowed by the keyed read"); + require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t entry) { return entry == outside; }), "image table unkeyed access: the table keyed per lane was narrowed"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); require(images != nullptr && images->count == 4u, "image table unkeyed access: the binding does not hold the whole domain"); } @@ -1519,18 +1817,19 @@ void verifyImageTableSrtKey() { const auto plan = GetResourcePlan(request); require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table SRT key: the T# was not planned as a table"); const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && !column->keyDomains.empty(), "image table SRT key: a key read through the SRT has no key domain"); + require(column.has_value() && column->selected && column->selection < plan->descriptorSources.size(), "image table SRT key: a T# whose key is read through the SRT does not select its record"); const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.codes.size() == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.codes[1] == outside && (snapshot.codes[2] & Abi::PoisonFlag) == 0u && snapshot.codes[3] == outside, "image table SRT key: the snapshot was not narrowed to the key read through the SRT"); + const auto& tables = compiled.capture->snapshot.tables; + const auto& snapshot = tables.tables.at(0); + require(snapshot.records == 1u && snapshot.keys == 1u && snapshot.codes.size() == 1u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && tables.words.at(snapshot.codes[0]).dwords == heap[2], "image table SRT key: the snapshot does not hold the selected record alone"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); - require(images != nullptr && images->count == 2u, "image table SRT key: the binding holds other entries than the selected one and the first"); - require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)), "image table SRT key: the key was not captured"); + require(images != nullptr && images->count == 1u, "image table SRT key: the binding holds other entries than the selected one"); + require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)) && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), "image table SRT key: the key or the selected record was not captured"); + require(!TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[1], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[3], sizeof(std::uint32_t)), "image table SRT key: records the key does not select were captured"); guest.srt[16] = 3u; const auto moved = CompileTable(request); - const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0); - require((movedSnapshot.codes[3] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[2] == outside && movedSnapshot.codes[1] == outside && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the snapshot alone"); + const auto& movedTables = moved.capture->snapshot.tables; + require(movedTables.words.at(movedTables.tables.at(0).codes.at(0)).dwords == heap[3] && TableCovered(moved.regions, &heap[3], sizeof(heap[3])) && !TableCovered(moved.regions, &heap[2], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the selected record alone"); } void verifyImageTableMaskedOffset() { @@ -1577,11 +1876,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); @@ -1818,6 +2130,281 @@ void verifyPointerTableOffsetCarry() { #endif } +void verifySelectedRecordFallbacks() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.materials = {}; + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableSrtKeyCode, userData); + const auto valid = CompileTable(request); + + heap[2][3] &= 0x0fffffffu; + const auto invalid = CompileTable(request); + const auto& invalidMap = TableMap(*invalid.result); + const auto invalidCodes = TableCodes(invalidMap, 0); + require(invalidCodes.size() == 1u && invalidCodes[0] != Abi::NullCode && (invalidCodes[0] & Abi::PoisonFlag) == 0u, "selected record fallbacks: an invalid selected T# has no entry"); + const auto invalidFlags = invalidMap.at(invalidCodes[0] + Abi::ImageRecordFlags); + require((invalidFlags & Abi::PoisonFlag) != 0u, "selected record fallbacks: an invalid selected T# was bound"); + const auto& poison = invalid.result->imageTablePoison.at(invalidFlags & ~Abi::PoisonFlag); + require(poison.reason == static_cast(Abi::PoisonReason::NotImage) && poison.dwordCount == 8u && poison.words[3] == heap[2][3] && invalid.result->imageTableFaults != 0u, "selected record fallbacks: an invalid selected T# was not poisoned with its words"); + require(invalid.result->variantId == valid.result->variantId, "selected record fallbacks: an invalid selected T# changed the artifact"); + heap[2] = heap[1]; + + guest.srt[16] = 200u; + const auto beyond = CompileTable(request); + const auto beyondCodes = TableCodes(TableMap(*beyond.result), 0); + require(beyondCodes.size() == 1u && beyondCodes[0] == Abi::NullCode && beyond.result->imageTableFaults == 0u && !TableTouched(beyond.regions, heap.data(), sizeof(heap)), "selected record fallbacks: a key past the heap did not select a null image without reading the heap"); + guest.srt[16] = 2u; + + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, 4096, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + require(reserved != nullptr, "selected record fallbacks: cannot reserve an inaccessible page"); + FillTableSrt(guest, reserved, 32u, 4u); + const auto unmapped = CompileTable(request); + const auto& unmappedTable = unmapped.capture->snapshot.tables.tables.at(0); + require(unmappedTable.keys == 1u && unmappedTable.codes.at(0) == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)) && !TableTouched(unmapped.regions, reserved, 4096u), "selected record fallbacks: an unmapped selected record is not Unmapped poison or was read"); + FillTableSrt(guest, heap.data(), 32u, 4u); + + std::vector loadedKey(TableSrtKeyCode); + const auto shift = std::find(loadedKey.begin(), loadedKey.end(), 0x8f108510u); + require(shift != loadedKey.end(), "selected record fallbacks: the program has no s_lshl_b32"); + loadedKey.insert(shift, {0xf4200406u, 0x20000000u}); + guest.materials[0][1] = 3u; + guest.srt[16] = 4u; + const auto loadedRequest = TableRequest(loadedKey, userData); + const auto loadedPlan = GetResourcePlan(loadedRequest); + require(loadedPlan->info.images.size() == 1u && loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a key loaded from a constant buffer at an SRT offset does not select its record"); + const auto loaded = CompileTable(loadedRequest); + require(TableSelected(loaded.capture->snapshot.tables, 0u, "selected record fallbacks: a loaded key does not hold one readable selected record") == heap[3] && TableCovered(loaded.regions, &guest.materials[0][1], sizeof(std::uint32_t)), "selected record fallbacks: the record of a loaded key was not selected or its key was not captured"); + const auto materials = TableV(guest.materials.data(), 16u, 3u); + const auto unreadable = TableV(reserved, 16u, 3u); + std::copy(unreadable.begin(), unreadable.end(), guest.srt.begin() + 8); + const auto lost = CompileTable(loadedRequest); + const auto& lostTable = lost.capture->snapshot.tables.tables.at(0); + require(lostTable.keys == 0u && lostTable.fault == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)) && lost.result->imageTableFaults != 0u && lost.result->variantId == loaded.result->variantId, "selected record fallbacks: a key that cannot be read did not fault its table"); + std::copy(materials.begin(), materials.end(), guest.srt.begin() + 8); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, 4096); +#endif + + auto writtenKey = loadedKey; + const auto store = std::find(writtenKey.begin(), writtenKey.end(), 0x80070000u); + require(store != writtenKey.end(), "selected record fallbacks: the program has no buffer_store_dword"); + *store = 0x80030000u; + const auto writtenRequest = TableRequest(writtenKey, userData); + const auto writtenPlan = GetResourcePlan(writtenRequest); + const auto& writtenColumn = writtenPlan->descriptorSources.at(writtenPlan->info.images.at(0).source).tableColumn; + require(writtenColumn.has_value() && !writtenColumn->selected && writtenColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a buffer the shader writes selected its record"); + const auto written = CompileTable(writtenRequest); + require(written.capture->snapshot.tables.tables.at(0).keys == 4u, "selected record fallbacks: a key loaded from a buffer the shader writes did not keep the whole table"); + require(!loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer has no key domain"); + auto pointerWritten = loadedKey; + const auto end = std::find(pointerWritten.begin(), pointerWritten.end(), 0xbf810000u); + require(end != pointerWritten.end(), "selected record fallbacks: the program has no s_endpgm"); + pointerWritten.insert(end, {0xdc708000u, 0x001c0100u}); + const auto pointerPlan = GetResourcePlan(TableRequest(pointerWritten, userData)); + const auto& pointerColumn = pointerPlan->descriptorSources.at(pointerPlan->info.images.at(0).source).tableColumn; + require(pointerPlan->info.bdaWrites && pointerColumn.has_value() && !pointerColumn->selected && pointerColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer in a shader that stores through a pointer selected its record or kept its key domain"); + + const auto lanePlan = GetResourcePlan(TableRequest(TableWholeCode, userData)); + require(!lanePlan->info.images.empty() && !lanePlan->descriptorSources.at(lanePlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a T# selected per lane left the table"); + + alignas(4096) static std::array, 70000> large{}; + large.back() = heap[1]; + FillTableSrt(guest, large.data(), 32u, static_cast(large.size())); + guest.srt[16] = static_cast(large.size() - 1u); + const auto pastCap = CompileTable(request); + require(TableSelected(pastCap.capture->snapshot.tables, 0u, "selected record fallbacks: a record past the key cap does not hold one readable selected record") == heap[1] && !pastCap.capture->snapshot.tables.tables[0].outside && pastCap.result->imageTableFaults == 0u, "selected record fallbacks: a record past the key cap was not selected"); + FillTableSrt(guest, heap.data(), 32u, 4u); +} + +void verifySelectedRecordOffsetCarry() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture texture; + alignas(4096) static TableGuest guest; + std::fill(guest.heap.begin(), guest.heap.end(), std::array{}); + const auto image = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D); + auto* words = guest.heap.front().data(); + std::copy(image.begin(), image.end(), words + 0x50u / 4u); + FillTableSrt(guest, guest.heap.data(), 0u, static_cast(sizeof(guest.heap))); + const auto userData = TableUserData(guest); + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0xf4080800u, 0xfa000010u, 0xf4000440u, 0xfa000040u, 0x8711ff11u, + 0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4280902u, 0x26000040u, 0xf4280a02u, 0x28000040u, 0xf09c0f08u, + 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto request = TableRequest(code, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "selected record offset carry: a T# read by two loads of its record was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.selected && !column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "selected record offset carry: the column is wrong"); + guest.srt[16] = 7u; + const auto carried = CompileTable(request); + const auto& carriedTable = carried.capture->snapshot.tables.tables.at(0); + require(carriedTable.keys == 1u && carriedTable.codes.at(0) == Abi::PoisonCode(static_cast(Abi::PoisonReason::OffsetCarry)) && !TableTouched(carried.regions, guest.heap.data(), sizeof(guest.heap)), "selected record offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison or was read"); + guest.srt[16] = 8u; + const auto wrapped = CompileTable(request); + require(TableSelected(wrapped.capture->snapshot.tables, 0u, "selected record offset carry: the record past the wrap does not hold one readable selected record") == image && TableCovered(wrapped.regions, words + 0x50u / 4u, sizeof(image)), "selected record offset carry: the record past the wrap was not selected"); +} + +std::vector WithImageOperation(std::vector code, std::uint32_t word0, bool keepSample) { + const auto sample = std::find(code.begin(), code.end(), 0xf09c0f08u); + require(sample != code.end() && sample + 1 != code.end(), "image operation variant: the program has no image_sample_lz"); + if (keepSample) { + code.insert(sample + 2, {word0, 0x00050000u}); + } else { + sample[0] = word0; + sample[1] = 0x00050000u; + } + return code; +} + +void verifySelectedRecordDirectOperations() { + using namespace ShaderRecompiler; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const std::array, 4> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}}; + for (const auto& [name, word0, format] : operations) { + const auto what = std::string("selected record direct operations: ") + name; + heap[2] = TableT(textures[1].bytes.data(), format, TableType2D); + const auto code = WithImageOperation(TableSrtKeyCode, word0, false); + const auto request = TableRequest(code, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == NoTable && !plan->descriptorSources.at(plan->info.images[0].source).tableColumn.has_value(), (what + " through a T# whose record the CPU evaluates was planned through an image table").c_str()); + const auto compiled = CompileTable(request); + require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); + const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); + const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); + } + const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); + const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); + require(mixed->info.images.size() == 2u && std::ranges::all_of(mixed->info.images, [&](const ImageResource& image) { return image.table == NoTable && !mixed->descriptorSources.at(image.source).tableColumn.has_value(); }), "selected record direct operations: a T# sampled and stored was planned through an image table for one of its operations"); + const auto sampled = GetResourcePlan(TableRequest(TableSrtKeyCode, userData)); + require(sampled->info.images.size() == 1u && sampled->info.images[0].table == 0u && sampled->descriptorSources.at(sampled->info.images[0].source).tableColumn->selected, "selected record direct operations: a T# that is only sampled no longer selects its record"); +} + +void verifyEvaluableHeapSamplerDirect() { + using namespace ShaderRecompiler; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto heapSampler = [](std::vector code) { + const auto direct = std::find(code.begin(), code.end(), 0xf4080200u); + require(direct != code.end() && direct[1] == 0xfa000010u, "evaluable heap sampler: the program has no S# load from the SRT"); + code.erase(direct, direct + 2); + const auto shift = std::find(code.begin(), code.end(), 0x8f108510u); + require(shift != code.end(), "evaluable heap sampler: the program has no key shift"); + code.insert(shift + 1, {0xf4280202u, 0x20000000u}); + return code; + }; + const auto evaluatedCode = heapSampler(TableSrtKeyCode); + const auto evaluated = GetResourcePlan(TableRequest(evaluatedCode, userData)); + require(evaluated->info.samplers.size() == 1u && evaluated->info.samplers[0].table == NoTable && !evaluated->descriptorSources.at(evaluated->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset the SRT walker evaluates was planned through a sampler table"); + const auto perLaneCode = heapSampler(TableWholeCode); + const auto perLane = GetResourcePlan(TableRequest(perLaneCode, userData)); + require(perLane->info.samplers.size() == 1u && perLane->info.samplers[0].table != NoTable && perLane->descriptorSources.at(perLane->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset keyed per lane was not planned through a sampler table"); +} + +void verifySelectedRecordUniformIndex() { + using namespace ShaderRecompiler; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto twoWaves = [&](const std::vector& code) { + auto request = TableRequest(code, userData); + request.context.compute->numThreads = {128u, 1u, 1u}; + return CompileTable(request); + }; + const auto nonUniform = [](const TableCompiled& compiled) { + const auto spirv = compiled.result->spirv.Words(); + std::uint32_t count = 0; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == spv::OpDecorate && (spirv[cursor] >> 16u) == 3u && spirv[cursor + 2u] == spv::DecorationNonUniform) ++count; + } + return count; + }; + const auto selected = twoWaves(TableSrtKeyCode); + require(TableBinding(*selected.result, DescriptorRole::ImageTable) != nullptr && nonUniform(selected) == 0u, "selected record uniform index: the image table index of a selected record in a two-wave workgroup is decorated NonUniform"); + const auto whole = twoWaves(TableWholeCode); + require(nonUniform(whole) != 0u, "selected record uniform index: the image table index of a table keyed per lane in a two-wave workgroup is not decorated NonUniform"); +} + +void verifyChainedTableOffset() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(256) static std::array root{}; + static std::array keys{}; + static std::array output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root.begin()); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + const auto at = [&](std::uint32_t offset, std::array words) { + std::copy(words.begin(), words.end(), root.begin() + offset / 4u); + }; + at(0x60u + 3u * 32u, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x60u + 5u * 32u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const auto base = static_cast(reinterpret_cast(root.data())); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const std::vector code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u, + 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113a012u, 0x81149013u, 0xf4080900u, 0x26000040u, 0xf4080a00u, 0x28000040u, 0xf09c0f08u, + 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x37000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "chained table offset: a T# whose second load offset adds to the first one's was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.addend == 0x20u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0x20u && column.foldedCarry == 16u, "chained table offset: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + const auto& words = compiled.capture->snapshot.tables.words; + require(snapshot.keys == 256u && (snapshot.codes[3] & Abi::PoisonFlag) == 0u && (snapshot.codes[5] & Abi::PoisonFlag) == 0u, "chained table offset: the records were not resolved"); + require(words.at(snapshot.codes[3]).dwords[0] == root[(0x60u + 3u * 32u) / 4u] && words.at(snapshot.codes[3]).dwords[7] == root[(0x60u + 3u * 32u) / 4u + 7u] && words.at(snapshot.codes[5]).dwords[0] == root[(0x60u + 5u * 32u) / 4u] && snapshot.codes[3] != snapshot.codes[5], "chained table offset: a record was read from the wrong offset"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -2039,15 +2626,15 @@ void verifyUniformTableImage() { request.layout.pushConstantSizeBytes = 128; const auto plan = GetResourcePlan(request); const auto& column = plan->descriptorSources.at(plan->info.images.at(0).source).tableColumn; - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u, "uniform table image: the T# read from a constant buffer at a user data offset was not planned as one table column"); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u && column->selected, "uniform table image: the T# read from a constant buffer at a user data offset does not select its record"); for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { userData[2] = key; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); const auto& tables = capture->snapshot.tables; - require(tables.tables.size() == 1u && key < tables.tables[0].codes.size(), "uniform table image: the capture has no table for the user data key"); - const auto code = tables.tables[0].codes[key]; - require((code & ImageTableAbi::PoisonFlag) == 0u && std::equal(expected.begin(), expected.end(), tables.words.at(code).dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + require(tables.tables.size() == 1u && TableSelected(tables, 0u, "uniform table image: a table does not hold one readable selected record") == expected, "uniform table image: the T# of the record the user data selects was not read"); + const auto regions = memory.Regions(); + require(TableCovered(regions, guest.table[key].data() + 4, sizeof(expected)) && !TableCovered(regions, guest.table[3u - key].data() + 4, sizeof(std::uint32_t)), "uniform table image: the record the user data does not select was read"); } } @@ -2418,6 +3005,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}; @@ -2673,6 +3299,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; @@ -3049,7 +3683,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; @@ -3167,7 +3801,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{ @@ -3258,6 +3892,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"); @@ -3266,6 +3901,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); @@ -3313,21 +3949,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)); @@ -4098,6 +4738,15 @@ void verifyUnnormalizedSamplers() { reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted"); reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted"); reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted"); + const auto reservedDword3 = [&](std::uint32_t mimg) { + auto code = program(mimg); + const std::array computed{0x7d8a0080u, 0x870bff6au, 0x000000ffu, 0x8f0b8c0bu}; + code.insert(code.begin(), computed.begin(), computed.end()); + return code; + }; + const auto adjusted = recompile(reservedDword3(0xf0800f09u), userData(normalized)); + require(proven(adjusted, false), "sampler dword 3: an image_sample_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled"); + expectFailure([&] { static_cast(recompile(reservedDword3(0xf0800f08u), userData(normalized))); }, "GetSamplerResource dword 3 is not a valid runtime value", "sampler dword 3: an image_sample whose S# dword 3 holds a ballot was accepted"); reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted"); @@ -4209,6 +4858,26 @@ void verifyUnnormalizedSamplers() { pixel.context.userData = normalizedPixelData; const auto pixelNormalized = Recompile(pixel); require(proven(pixelNormalized, false) && implicitSamples(pixelNormalized.spirv.Words()) == 1u, "unnormalized samplers: a pixel image_sample through a normalized S# is not an implicit-LOD sample"); + const std::array pixelOffsetCode{0x7e0202ffu, 0x00000201u, 0xf0c00f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + pixel.shader = {ShaderStage::Fragment, 0x50000u, pixelOffsetCode, 0, {}}; + pixel.context.userData = pixelData; + pixel.target.supportedCapabilities = capabilities; + pixel.target.nonConstantImageOffsets = true; + const auto pixelOffsetUnnormalized = Recompile(pixel); + require(proven(pixelOffsetUnnormalized, true), "unnormalized texel offsets: a pixel image_sample_o through an unnormalized S# was not flagged"); + auditSpirv(pixelOffsetUnnormalized.spirv.Words()); + require(foldedSamples(pixelOffsetUnnormalized.spirv.Words()), "unnormalized texel offsets: a pixel image_sample_o offset was not added to the base-level coordinates"); + pixel.context.userData = normalizedPixelData; + const auto pixelOffsetNormalized = Recompile(pixel); + std::vector implicitMasks; + const auto offsetWords = pixelOffsetNormalized.spirv.Words(); + for (std::size_t cursor = 5; cursor < offsetWords.size();) { + const auto count = offsetWords[cursor] >> 16u; + require(count != 0 && count <= offsetWords.size() - cursor, "unnormalized texel offsets: truncated SPIR-V instruction"); + if ((offsetWords[cursor] & 0xffffu) == spv::OpImageSampleImplicitLod && count > 5u) implicitMasks.push_back(offsetWords[cursor + 5]); + cursor += count; + } + require(proven(pixelOffsetNormalized, false) && implicitMasks.size() == 1u && (implicitMasks[0] & (spv::ImageOperandsConstOffsetMask | spv::ImageOperandsOffsetMask)) != 0u, "unnormalized texel offsets: a normalized pixel image_sample_o is not an implicit-LOD sample with its offset"); } void verifyUnusedUnnormalizedSampler() { @@ -4290,6 +4959,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"); @@ -5094,6 +5773,25 @@ void verifyVolumeArrayLoadMode() { expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); } +void verifyDynamicStorageMipChain() { + using namespace ShaderRecompiler; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Storage; + image.numericClass = IrTextureNumericClass::Float; + image.dimension = RdnaImageDimension::Dim2D; + image.mipMode = ImageMipMode::DynamicStorage; + image.written = true; + image.fmaskCompatible = false; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const DescriptorValue tenLevels{{0x12366e00u, 0xc2400000u, 0x007fc07fu, 0xd1b903acu, 0x17u, 0x00700090u, 0u, 0u}, 8u}; + const auto selected = ResourceMaterializer::TryRuntimeImageMode(image, tenLevels, modes); + require(selected.mode.has_value(), "a dynamic-mip storage image of a 512x512 surface with ten levels was rejected"); + require(modes[*selected.mode].mipCount >= 10u, "a dynamic-mip storage mode does not hold ten levels"); + auto allLevels = tenLevels; + allLevels.dwords[3] |= 0xf0000u; + require(ResourceMaterializer::TryRuntimeImageMode(image, allLevels, modes).mode.has_value(), "a dynamic-mip storage image with sixteen levels was rejected"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -5130,10 +5828,17 @@ 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(); + verifyImageTableScratchReuse(); + verifyImageTableBulkKeyScan(); verifyImageTableSrtKey(); verifyImageTableMaskedOffset(); verifyImageTableKeyUnion(); @@ -5143,6 +5848,12 @@ int main(int argc, char** argv) { verifyScalarAddressRule(); verifyPointerImageTable(); verifyPointerTableOffsetCarry(); + verifySelectedRecordFallbacks(); + verifySelectedRecordOffsetCarry(); + verifySelectedRecordDirectOperations(); + verifyEvaluableHeapSamplerDirect(); + verifySelectedRecordUniformIndex(); + verifyChainedTableOffset(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -5168,12 +5879,14 @@ int main(int argc, char** argv) { verifyMayEvaluate(); verifyPureFlatSlots(); verifyGuardedDescriptorSources(); + verifySrtTape(); verifyImageTableKeys(); verifyImageTables(); verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); verifyPointerTableOffsetCarry(); + verifyChainedTableOffset(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); @@ -5208,6 +5921,7 @@ int main(int argc, char** argv) { verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); verifyQuadMaskNeighbourReads(); verifyCoherentGds(); 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/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index c72b4f3b02..6c6acca9d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -32,6 +32,7 @@ struct ElementAddress { std::uint32_t y; std::uint32_t z; std::uint64_t address; + std::uint32_t pipeBankXor = 0; }; std::uint64_t equationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { @@ -60,7 +61,7 @@ void requireThickAddresses(const GuestTextureResource& resource, std::uint32_t b const auto geometry = DescribeSurface(resource); Require(geometry.thick && geometry.mips.size() == resource.mipCount && geometry.guestBytes == guestBytes, what + ": guest size differs from addrlib"); for (std::uint32_t level = 0; level < resource.mipCount; ++level) Require(geometry.mips[level].tail == (level >= firstTailLevel), what + ": mip " + std::to_string(level) + " is on the wrong side of the mip tail"); - const auto blockBytes = resource.tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u; + const auto blockBytes = resource.tileMode == TextureTileMode::kStandard4KB || resource.tileMode == TextureTileMode::kS4KBX ? 4096u : 65536u; const auto* equation = FindTextureSwizzleEquation(0x100u | (resource.tileMode == TextureTileMode::kStandard4KB ? 5u : resource.tileMode == TextureTileMode::kStandard64KB ? 9u : XorSwizzleMode(resource.tileMode)), bytesPerElement); Require(equation != nullptr, what + ": missing thick swizzle equation"); const auto block = ThickBlockExtent(resource.tileMode, bytesPerElement); @@ -73,6 +74,7 @@ void requireThickAddresses(const GuestTextureResource& resource, std::uint32_t b } else { address += (static_cast(element.y / block[1]) * mip.blocksPerRow + element.x / block[0]) * blockBytes + equationOffset(*equation, element.x, element.y, element.z); } + address ^= element.pipeBankXor; Require(address == element.address, what + ": mip " + std::to_string(element.mip) + " element (" + std::to_string(element.x) + ", " + std::to_string(element.y) + ", " + std::to_string(element.z) + ") detiles from " + std::to_string(address) + " instead of addrlib's " + std::to_string(element.address)); } } @@ -258,6 +260,22 @@ void RunTextureTilingTests() { Require(odd.mips[0].blocksPerRow == 5 && odd.mips[0].tiledOffset == 12288 && odd.mips[0].tiledSize == 40960, "3D mip 0 of a non-power-of-two volume changed"); } + { + constexpr ElementAddress s4xTail1[] = {{0, 0, 0, 0, 0x3000, 0x0}, {0, 32, 19, 11, 0x843c, 0x0}, {0, 8, 9, 8, 0x3e08, 0x0}, {0, 15, 2, 8, 0x3163, 0x700}, {1, 0, 0, 0, 0x1000, 0x0}, {1, 15, 9, 5, 0x1acf, 0x0}, {1, 12, 4, 0, 0x1340, 0x0}, {1, 7, 2, 5, 0x17e7, 0x700}, {2, 0, 0, 0, 0x800, 0x0}, {2, 7, 4, 2, 0x953, 0x0}, {2, 2, 2, 0, 0x822, 0x0}, {2, 0, 1, 1, 0xf0c, 0x700}, {3, 0, 0, 0, 0x300, 0x0}, {3, 3, 1, 0, 0x30b, 0x0}, {3, 3, 1, 0, 0x30b, 0x0}, {3, 0, 0, 0, 0x400, 0x700}, {4, 0, 0, 0, 0x200, 0x0}, {4, 1, 0, 0, 0x201, 0x0}, {4, 0, 0, 0, 0x200, 0x0}, {4, 0, 0, 0, 0x500, 0x700}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x600, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 1, 33, 20, 12, 6), 1, 2, 36864, s4xTail1, "SW_4KB_S_X 8 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xTail2[] = {{0, 0, 0, 0, 0x6000, 0x0}, {0, 32, 19, 11, 0xf13c, 0x0}, {0, 8, 9, 8, 0x7408, 0x0}, {0, 15, 2, 8, 0x7962, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x42ce, 0x0}, {1, 12, 4, 0, 0x4b00, 0x0}, {1, 7, 2, 5, 0x35e6, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x1352, 0x0}, {2, 2, 2, 0, 0x1060, 0x0}, {2, 0, 1, 1, 0x170c, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x84a, 0x0}, {3, 3, 1, 0, 0x84a, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x302, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 12, 33, 20, 12, 6), 2, 3, 65536, s4xTail2, "SW_4KB_S_X 16 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xWide4[] = {{0, 0, 0, 0, 0x20000, 0x0}, {0, 127, 127, 7, 0x9f4fc, 0x0}, {0, 24, 93, 0, 0x73108, 0x0}, {0, 42, 22, 4, 0x35f60, 0x700}, {1, 0, 0, 0, 0x0, 0x0}, {1, 63, 63, 3, 0x1f0fc, 0x0}, {1, 28, 38, 2, 0x13da0, 0x0}, {1, 55, 52, 1, 0x1e754, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 56, 128, 128, 8, 2), 4, 2, 655360, s4xWide4, "SW_4KB_S_X 32 bpp 128x128x8, 2 levels"); + constexpr ElementAddress s4xTail4[] = {{0, 0, 0, 0, 0x6000, 0x0}, {0, 32, 19, 11, 0x1fdb8, 0x0}, {0, 8, 9, 8, 0x17808, 0x0}, {0, 15, 2, 8, 0x17564, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x465c, 0x0}, {1, 12, 4, 0, 0x4b00, 0x0}, {1, 7, 2, 5, 0x3174, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x13c4, 0x0}, {2, 2, 2, 0, 0x1060, 0x0}, {2, 0, 1, 1, 0x1718, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x84c, 0x0}, {3, 3, 1, 0, 0x84c, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x304, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 56, 33, 20, 12, 6), 4, 3, 131072, s4xTail4, "SW_4KB_S_X 32 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xTail8[] = {{0, 0, 0, 0, 0x9000, 0x0}, {0, 32, 19, 11, 0x2fab0, 0x0}, {0, 8, 9, 8, 0x27420, 0x0}, {0, 15, 2, 8, 0x22448, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x7a78, 0x0}, {1, 12, 4, 0, 0x4200, 0x0}, {1, 7, 2, 5, 0x3058, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x1ac8, 0x0}, {2, 2, 2, 0, 0x1140, 0x0}, {2, 0, 1, 1, 0x1730, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x868, 0x0}, {3, 3, 1, 0, 0x868, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x308, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 71, 33, 20, 12, 6), 8, 3, 196608, s4xTail8, "SW_4KB_S_X 64 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xTail16[] = {{0, 0, 0, 0, 0x10000, 0x0}, {0, 32, 19, 11, 0x55bb0, 0x0}, {0, 8, 9, 8, 0x46820, 0x0}, {0, 15, 2, 8, 0x3e040, 0x700}, {1, 0, 0, 0, 0x6000, 0x0}, {1, 15, 9, 5, 0xee70, 0x0}, {1, 12, 4, 0, 0x9400, 0x0}, {1, 7, 2, 5, 0x7850, 0x700}, {2, 0, 0, 0, 0x3000, 0x0}, {2, 7, 4, 2, 0x42c0, 0x0}, {2, 2, 2, 0, 0x3300, 0x0}, {2, 0, 1, 1, 0x3730, 0x700}, {3, 0, 0, 0, 0x1000, 0x0}, {3, 3, 1, 0, 0x1260, 0x0}, {3, 3, 1, 0, 0x1260, 0x0}, {3, 0, 0, 0, 0x1700, 0x700}, {4, 0, 0, 0, 0x800, 0x0}, {4, 1, 0, 0, 0x840, 0x0}, {4, 0, 0, 0, 0x800, 0x0}, {4, 0, 0, 0, 0xf00, 0x700}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x400, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 77, 33, 20, 12, 6), 16, 4, 352256, s4xTail16, "SW_4KB_S_X 128 bpp 33x20x12, 6 levels"); + requireBlockVolumeLayout(TextureTileMode::kS4KBX, 169, 71, 60, 36, 20, 5, "SW_4KB_S_X BC1 60x36x20, 5 levels"); + } + { constexpr ElementAddress chainInTail[] = {{0, 0, 0, 0, 0x800}, {0, 7, 7, 7, 0xffc}, {0, 4, 2, 4, 0xe20}, {0, 2, 7, 0, 0x968}, {0, 7, 0, 7, 0xed4}, {1, 0, 0, 0, 0x300}, {1, 3, 3, 3, 0x3fc}, {1, 2, 1, 2, 0x3c8}, {1, 1, 3, 0, 0x32c}, {1, 3, 0, 3, 0x3d4}}; requireThickAddresses(thickVolume(TextureTileMode::kStandard4KB, 56, 8, 8, 8, 2), 4, 0, 4096, chainInTail, "SW_4KB_S 32 bpp 8x8x8, 2 levels"); 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/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index d69035db8c..b393d0830d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -9,6 +9,7 @@ #include #else #include +#include #endif #include #include @@ -280,6 +281,75 @@ void CheckNewestStamp() { Require(NewestStamp(base + 3 * Block - 8, Block + 8) > partial, "a CPU store into the watched part of a partly watched range did not advance its stamp"); } +void CheckPartlyWatchedStore() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto stored = base + Block; + const auto unwatched = base + 2 * Block; + std::memset(memory, 0x11, 3 * Block); + BumpCollectEpoch(); + Unwatch(unwatched, Block); + const auto before = CollectWrites(base, 2 * Block); + Require(before != 0 && UnchangedSince(base, 2 * Block, before), "the watched blocks were not collected"); + Require(MarkWritten(unwatched - 64, 128) == 0, "a driver store into partly watched memory is trusted"); + Require(!UnchangedSince(stored, Block, before), "a driver store into partly watched memory left its watched block unchanged"); + Require(StoredOver(unwatched - 64, 64, before), "a driver store into partly watched memory is not seen over its watched bytes"); + Require(!StoredOver(stored, Block - 64, before), "a driver store into partly watched memory is seen over watched bytes it did not store"); + Require(UnchangedSince(base, Block, before), "a driver store into partly watched memory stamped a block it did not store into"); + const auto beforeOwn = TrackerGeneration(); + Require(StoreOwnBytes(unwatched - 64, 128, [&] { std::memset(reinterpret_cast(unwatched - 64), 0x22, 128); }) == 0, "an own store into partly watched memory is trusted"); + Require(!UnchangedSince(stored, Block, beforeOwn), "an own store into partly watched memory left its watched block unchanged"); + Require(UnchangedSince(base, Block, beforeOwn), "an own store into partly watched memory stamped a block it did not store into"); +#ifndef _WIN32 + void* split = AllocateWatched(Block); + const auto splitBase = reinterpret_cast(split); + const auto watchedHalf = splitBase + Block / 2; + std::memset(split, 0x11, Block); + Unwatch(splitBase, Block / 2); + const auto beforeSplit = CollectWrites(watchedHalf, Block / 2); + Require(beforeSplit != 0 && UnchangedSince(watchedHalf, Block / 2, beforeSplit), "the watched half of a split block was not collected"); + Require(MarkWritten(splitBase + 64, 128) == 0 && UnchangedSince(watchedHalf, Block / 2, beforeSplit), "a driver store into the unwatched half of a split block stamped its watched half"); + Require(MarkWritten(watchedHalf - 64, 128) == 0, "a driver store across the halves of a split block is trusted"); + Require(!UnchangedSince(watchedHalf, Block / 2, beforeSplit), "a driver store across the halves of a split block left its watched half unchanged"); +#endif +} + +#ifndef _WIN32 +void CheckWatchOverlaps() { + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* memory = AllocateWatched(Block); + const auto base = reinterpret_cast(memory); + Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, page), "cannot unwatch the block's first page"); + Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(base + 3 * page), Block - 3 * page), "cannot unwatch the block's last pages"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(base, Block), "a block with unwatched pages is covered"); + Require(GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base, Block), "a block with watched pages inside does not overlap the watch"); + Require(GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + page - 1, 2), "a range ending in a watched page does not overlap the watch"); + Require(GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + 3 * page - 1, 2), "a range starting in a watched page does not overlap the watch"); + Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base, page), "an unwatched page before the watched ones overlaps the watch"); + Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + 3 * page, Block - 3 * page), "unwatched pages after the watched ones overlap the watch"); + Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + page, 0), "an empty range overlaps the watch"); +} + +void CheckWatchedMiddleStore() { + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* memory = AllocateWatched(Block); + const auto base = reinterpret_cast(memory); + const auto middle = base + page; + std::memset(memory, 0x11, Block); + Unwatch(base, page); + Unwatch(base + 3 * page, Block - 3 * page); + const auto before = CollectWrites(middle, 2 * page); + Require(before != 0 && UnchangedSince(middle, 2 * page, before), "the watched middle of a block was not collected"); + Require(MarkWritten(base + 3 * page + 64, 128) == 0 && UnchangedSince(middle, 2 * page, before), "a driver store into an unwatched end of a block stamped its watched middle"); + Require(MarkWritten(base + page / 2, 3 * page) == 0, "a driver store around the watched middle of a block is trusted"); + Require(!UnchangedSince(middle, 2 * page, before), "a driver store around the watched middle of a block left it unchanged"); + Require(StoredOver(middle, 2 * page, before), "a driver store around the watched middle of a block is not seen over it"); + const auto beforeOwn = TrackerGeneration(); + Require(StoreOwnBytes(base + page / 2, 3 * page, [&] { std::memset(reinterpret_cast(base + page / 2), 0x22, 3 * page); }) == 0, "an own store around the watched middle of a block is trusted"); + Require(!UnchangedSince(middle, 2 * page, beforeOwn), "an own store around the watched middle of a block left it unchanged"); +} +#endif + #ifdef _WIN32 void CheckDeferredSharedImports() { constexpr std::size_t bytes = 4 * Block; @@ -372,6 +442,49 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } +void CheckRewatch() { + void* memory = GuestArena::GuestArenaAllocate_nid_postfix(4 * Block, Block); + GuestArena::GuestArenaCommit_nid_postfix(memory, 4 * Block, PAGE_READWRITE, 16384); + const auto base = reinterpret_cast(memory); + Require(Watched(base, 4 * Block), "the rewatch test block is not watched"); + auto* bytes = static_cast(memory); + const auto imported = base + Block; + std::memset(memory, 0x11, 4 * Block); + BumpCollectEpoch(); + const auto before = CollectWrites(base, 4 * Block); + Require(before != 0, "the test range was not collected"); + Unwatch(imported, 2 * Block); + const auto unwatched = UnwatchSerial(); + Require(!Watched(imported, Block) && UntrackedHint(imported, Block) != 0, "the unwatched block does not read as untracked"); + bytes[Block + 8] = 0x22; + const std::array, 1> partial{{{imported + 4096, imported + Block}}}; + Rewatch(partial); + Require(!Watched(imported, Block) && UnwatchSerial() == unwatched, "part of a block returned to the write watch"); + const std::array, 1> first{{{imported, imported + Block}}}; + Rewatch(first); + Require(Watched(imported, Block) && !Watched(imported + Block, Block), "rewatching did not return exactly its block to the write watch"); + Require(UnwatchSerial() != unwatched, "rewatching did not invalidate what was cached while the block was unwatched"); + Require(UntrackedHint(imported, Block) == 0, "a rewatched block still reads as untracked"); + Require(!UnchangedSince(imported, Block, before) && StoredOver(imported, Block, before), "a block returned to the write watch reads as unchanged since before it left"); + Require(!UnchangedSinceCollected(imported, Block, before), "a CPU store made while the block was unwatched reads as no CPU store"); + Require(UnchangedSince(base, Block, before) && UnchangedSince(base + 3 * Block, Block, before), "rewatching stamped the blocks around it"); + BumpCollectEpoch(); + const auto after = CollectWrites(imported, Block); + Require(after != 0 && UnchangedSince(imported, Block, after), "a rewatched block is not collected"); + bytes[Block + 16] = 0x33; + CollectWritesUncached(imported, Block); + Require(!UnchangedSince(imported, Block, after), "a CPU store after rewatching was missed"); + const auto serial = UnwatchSerial(); + Rewatch(first); + Require(UnwatchSerial() == serial, "rewatching a watched block changed what it covers"); + const std::array, 1> uncommitted{{{base + 2 * Block, base + 3 * Block}}}; + GuestArena::GuestArenaReset_nid_postfix(static_cast(memory) + 2 * Block, 16384); + Rewatch(uncommitted); + Require(!Watched(base + 2 * Block, Block), "a block with uncommitted pages returned to the write watch"); + GuestArena::GuestArenaReset_nid_postfix(memory, 4 * Block); + GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block); +} + void CheckUntrackedHint() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); @@ -730,6 +843,24 @@ void ProtectPage(std::uint64_t address, bool readable) { GuestAllocations::GuestAllocationsInvalidate_nid_postfix(static_cast(address), 4096); } +void CheckStoreAcrossImports() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto boundary = base + 2 * Block; + std::memset(memory, 0x11, 3 * Block); + Require(ImportWatched(base, 2 * Block, [] { return true; }) && Watched(base, 2 * Block), "an import that stays watched left the watch"); + Require(ImportWatched(boundary, Block, [] { return true; }, false) && !Watched(boundary, Block), "an import that leaves the watch stayed watched"); + const auto before = CollectWrites(base, 2 * Block); + Require(before != 0 && UnchangedSince(base, 2 * Block, before), "the watched import was not collected"); + Require(MarkWritten(boundary - 64, 128) == 0, "a GPU write across a watched import and an unwatched one is trusted"); + Require(!UnchangedSince(base + Block, Block, before), "a GPU write across a watched import and an unwatched one left the watched import's last block unchanged"); + Require(StoredOver(boundary - 64, 64, before) && !StoredOver(base + Block, Block - 64, before), "a GPU write across a watched import and an unwatched one is not seen over exactly its watched bytes"); + Require(UnchangedSince(base, Block, before), "a GPU write across a watched import and an unwatched one stamped a block it did not write"); + const auto beforeOwn = TrackerGeneration(); + Require(StoreOwnBytes(boundary - 64, 128, [&] { std::memset(reinterpret_cast(boundary - 64), 0x22, 128); }) == 0, "an own store across a watched import and an unwatched one is trusted"); + Require(!UnchangedSince(base + Block, Block, beforeOwn) && UnchangedSince(base, Block, beforeOwn), "an own store across a watched import and an unwatched one did not stamp exactly the watched block it wrote"); +} + void CheckUnwatchedImport() { void* memory = AllocateWatched(3 * Block); const auto base = reinterpret_cast(memory); @@ -935,9 +1066,15 @@ int main() { CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); + CheckPartlyWatchedStore(); +#ifndef _WIN32 + CheckWatchOverlaps(); + CheckWatchedMiddleStore(); +#endif #ifdef _WIN32 CheckDeferredSharedImports(); CheckPrivateMappingReuse(); + CheckRewatch(); CheckUntrackedHint(); CheckHostHoleInChild(); #endif @@ -946,6 +1083,7 @@ int main() { CheckImportWindow(); CheckImportWindowZeroPages(); CheckImportWindowOtherThread(); + CheckStoreAcrossImports(); CheckUnwatchedImport(); CheckCommittedRanges(); } catch (const std::exception& error) { 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/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index e9668e78dc..9f844dd5cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -680,6 +680,10 @@ void RunImportWindow(AgcDriver::VulkanDevice& device, std::uint8_t* block) { #ifdef _WIN32 void RunHintedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { using namespace AgcDriver::GuestMemory; + AgcDriver::Graphics::SetHostImportDeferredSettles(false); + struct Restore { + ~Restore() { AgcDriver::Graphics::SetHostImportDeferredSettles(true); } + } restore; const auto address = AddressOf(block); std::memset(block, 0x55, BlockBytes); auto descriptor = TextureDescriptor(block, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp new file mode 100644 index 0000000000..4732c5c876 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp @@ -0,0 +1,252 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 192; +constexpr std::uint32_t Texels = Width * Height; +constexpr std::uint32_t Format16UInt = 11; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Swizzle64KbZX = 24; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t Unwritten = 0xa5a5a5a5u; +constexpr std::size_t SlotBytes = 256u << 10u; +constexpr std::size_t BlockBytes = 1u << 20u; + +static_assert(Width == 256u, "the row shader scales the row by 256 texels"); + +alignas(256) constexpr std::array RowCode{ + 0x7e020210u, 0x34040288u, 0x4a040500u, 0x34040482u, 0xf0001108u, 0x00010300u, 0xbf8c3f70u, 0xe0701000u, + 0x80000302u, 0xbf810000u, +}; + +constexpr std::array Edges32{ + 0x00000000u, 0x00000001u, 0x12345678u, 0x7f7fffffu, 0x3f800000u, 0x80000000u, 0xbf800000u, 0x00800000u, + 0x007fffffu, 0x80000001u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800001u, 0xffffffffu, 0x7fbfffffu, +}; + +constexpr std::array Edges16{ + 0x0000u, 0x0001u, 0x1234u, 0x7fffu, 0x8000u, 0x8001u, 0xfffeu, 0xffffu, +}; + +alignas(4096) std::array UntrackedStorage{}; +alignas(256) std::array UntrackedOutput{}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "depth bits guest memory: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Slots() { return block; } + std::span Output() { return {reinterpret_cast(block + 3u * SlotBytes), Texels}; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Memory { + const char* name; + std::uint8_t* slots; + std::span output; +}; + +struct Case { + const char* name; + std::uint32_t format; + std::uint32_t elementBytes; + std::uint32_t slot; +}; + +constexpr std::array Cases{{ + {"32_UINT", Format32UInt, 4u, 0u}, + {"32_SINT", Format32SInt, 4u, 1u}, + {"16_UINT", Format16UInt, 2u, 2u}, +}}; + +static_assert(Texels * 4u <= SlotBytes && 4u * SlotBytes <= BlockBytes, "the slots and the output must fit the guest block"); + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Mix(std::uint32_t x, std::uint32_t y) { + auto hash = (x * 0x9e3779b1u) ^ (y * 0x85ebca77u); + hash ^= hash >> 15u; + hash *= 0x2c1b3c6du; + hash ^= hash >> 12u; + return hash; +} + +std::uint32_t Pattern(const Case& test, std::uint32_t x, std::uint32_t y) { + const bool edge = (x & 15u) == (y & 15u); + if (test.elementBytes == 2u) return edge ? Edges16[(x / 16u + y) % Edges16.size()] : Mix(x, y) & 0xffffu; + return edge ? Edges32[(x / 16u + y) % Edges32.size()] : Mix(x, y); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + IdentitySwizzle | (Swizzle64KbZX << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::uint64_t EquationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < 16; ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12) & (mask & 0xfff000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +std::uint8_t* SlotOf(const Memory& memory, const Case& test) { + return memory.slots + test.slot * SlotBytes; +} + +void Fill(const Memory& memory, const Case& test) { + auto* texels = SlotOf(memory, test); + const auto descriptor = DecodeTextureResource(TextureDescriptor(texels, test.format)); + Require(descriptor.baseAddress == reinterpret_cast(texels) && descriptor.pipeBankXor == 0u, std::string(test.name) + ": the texels are not 64 KiB aligned"); + const auto geometry = DescribeSurface(descriptor); + Require(geometry.guestBytes <= SlotBytes, std::string(test.name) + ": the surface does not fit its slot"); + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(descriptor.tileMode), test.elementBytes); + Require(equation != nullptr, std::string(test.name) + ": the SW_64KB_Z_X equation is missing"); + const auto& mip = geometry.mips.at(0); + Require(!mip.tail, std::string(test.name) + ": the base level is a tail mip"); + const auto layout = ThinBlockLayout(descriptor.tileMode, test.elementBytes); + std::fill(texels, texels + SlotBytes, std::uint8_t{0xee}); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = mip.tiledOffset + (static_cast(y / layout[2]) * mip.blocksPerRow + x / layout[1]) * layout[0] + EquationOffset(*equation, x, y); + Require(offset + test.elementBytes <= geometry.guestBytes, std::string(test.name) + ": a texel lies outside the surface"); + const auto value = Pattern(test, x, y); + std::memcpy(texels + offset, &value, test.elementBytes); + } + } +} + +void Read(AgcDriver::VulkanDevice& device, const Memory& memory, const Case& test) { + std::fill(memory.output.begin(), memory.output.end(), Unwritten); + std::vector userData(16, 0u); + const auto output = BufferDescriptor(memory.output.data(), static_cast(memory.output.size_bytes())); + const auto texture = TextureDescriptor(SlotOf(memory, test), test.format); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(RowCode); + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Width, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, regions}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, Height, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Memory& memory, const Case& test) { + std::uint32_t wrong = 0; + std::string first; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = Pattern(test, x, y); + const auto actual = memory.output[y * Width + x]; + if (actual == expected) continue; + if (wrong == 0) first = "(" + std::to_string(x) + ", " + std::to_string(y) + ") read " + Hex(actual) + ", expected " + Hex(expected); + ++wrong; + } + } + Require(wrong == 0, std::string(test.name) + " SW_64KB_Z_X depth bits from " + memory.name + ": " + std::to_string(wrong) + " of " + std::to_string(Texels) + " texels differ, first " + first); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto* untracked = UntrackedStorage.data() + ((0u - reinterpret_cast(UntrackedStorage.data())) & 0xffffu); + const std::array memories{{ + {"untracked memory", untracked, UntrackedOutput}, + {"a guest allocation", block.Slots(), block.Output()}, + }}; + for (const auto& memory : memories) { + for (const auto& test : Cases) { + Fill(memory, test); + Read(*device, memory, test); + Check(memory, test); + } + } + std::puts("depth bits guest memory tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp new file mode 100644 index 0000000000..93f7886211 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp @@ -0,0 +1,516 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "DepthFastClearHarness.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 AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +namespace Graphics = AgcDriver::Graphics; + +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Format16Uint = 11; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format32Uint = 20; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t TileLinear = 0; +constexpr std::uint32_t TileZ64KBX = 0x18; + +constexpr std::array ClearKernel{0xd7460004u, 0x0401060cu, 0xf4280204u, 0xfa000000u, 0xd7460005u, 0x0405060du, 0x7e0c020eu, 0xbf8cc07fu, 0x7e000208u, 0x7e020209u, 0x7e04020au, 0x7e06020bu, 0xf0200f28u, 0x00000004u, 0xbf810000u}; + +class GuestBlock { +public: + GuestBlock() { + constexpr std::size_t bytes = 65536; +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(bytes, bytes)); +#endif + Require(block != nullptr, "depth clear: cannot allocate a guest block"); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +std::uint32_t Low(std::uint64_t value) { return static_cast(value); } + +std::uint32_t High(std::uint64_t value) { return static_cast(value >> 32u); } + +std::vector Pm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Submit(std::vector> packets) { + std::vector words; + for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end()); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "depth clear: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +std::array TextureDescriptor(std::uint64_t address, std::uint32_t format, std::uint32_t tile = TileLinear, std::uint32_t type = Type2DArray) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (tile << 20u) | (type << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t bytes) { + return {Low(address), High(address) & 0xffffu, bytes, 0x31016facu}; +} + +void DispatchClear(const GuestBlock& code, const GuestBlock& plane, const GuestBlock& color, std::uint32_t format, std::uint32_t groupsX, std::uint32_t groupsY, std::uint32_t tile = TileLinear) { + const auto kernel = code.Address(); + const auto texture = TextureDescriptor(plane.Address(), format, tile); + const auto buffer = BufferDescriptor(color.Address(), 16u); + std::vector> packets{ + Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), + Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), + Pm4Packet(0x76, {0x207, 8}), + Pm4Packet(0x76, {0x208, 8}), + Pm4Packet(0x76, {0x209, 1}), + Pm4Packet(0x76, {0x213, (12u << 1u) | 0x80u | 0x100u | 0x200u | (1u << 11u)}), + }; + for (std::uint32_t word = 0; word < 8u; ++word) packets.push_back(Pm4Packet(0x76, {0x240u + word, texture[word]})); + for (std::uint32_t word = 0; word < 4u; ++word) packets.push_back(Pm4Packet(0x76, {0x248u + word, buffer[word]})); + packets.push_back(Pm4Packet(0x15, {groupsX, groupsY, 1, 0x41})); + Submit(std::move(packets)); +} + +void RunClear(AgcDriver::VulkanDevice& device, const GuestBlock& code, const GuestBlock& plane, const GuestBlock& color, std::uint32_t format, std::uint32_t groupsX, std::uint32_t groupsY, std::uint32_t tile = TileLinear) { + std::vector userData(12); + const auto texture = TextureDescriptor(plane.Address(), format, tile); + const auto buffer = BufferDescriptor(color.Address(), 16u); + std::copy(texture.begin(), texture.end(), userData.begin()); + std::copy(buffer.begin(), buffer.end(), userData.begin() + 8); + const std::span kernel(reinterpret_cast(code.Data()), ClearKernel.size()); + const ShaderRecompiler::ShaderComputeStageInfo compute{{8, 8, 1}, 0u, {true, true, true}, false, 2u, {0, 0, 0}}; + const std::array memory{{ + {static_cast(code.Address()), std::as_bytes(kernel)}, + {static_cast(color.Address()), std::span(reinterpret_cast(color.Data()), 16)}, + }}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, static_cast(code.Address()), kernel, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::optional scope; + if (const auto clear = Graphics::MatchStorageImageClear(kernel, userData, compute, {groupsX, groupsY, 1u})) scope.emplace(*clear); + device.Dispatch(program, groupsX, groupsY, 1, {}, static_cast(code.Address())); + scope.reset(); + device.WaitIdle(); +} + +void SetColor(GuestBlock& color, float value) { + const std::array channels{value, value, value, value}; + std::memcpy(color.Data(), channels.data(), sizeof(channels)); + AgcDriver::GuestMemory::MarkWritten(color.Address(), sizeof(channels)); +} + +void SetRaw(GuestBlock& color, std::uint32_t raw) { + const std::array channels{raw, 0u, 0u, 0u}; + std::memcpy(color.Data(), channels.data(), sizeof(channels)); + AgcDriver::GuestMemory::MarkWritten(color.Address(), sizeof(channels)); +} + +Graphics::DepthClearStatistics Difference(const Graphics::DepthClearStatistics& now, const Graphics::DepthClearStatistics& before) { + Graphics::DepthClearStatistics difference; + difference.matched = now.matched - before.matched; + difference.taken = now.taken - before.taken; + difference.copiesSkipped = now.copiesSkipped - before.copiesSkipped; + difference.clears = now.clears - before.clears; + difference.bytesSkipped = now.bytesSkipped - before.bytesSkipped; + for (std::size_t reason = 0; reason < difference.declined.size(); ++reason) difference.declined[reason] = now.declined[reason] - before.declined[reason]; + return difference; +} + +std::uint64_t Declined(const Graphics::DepthClearStatistics& statistics) { + std::uint64_t total = 0; + for (const auto count : statistics.declined) total += count; + return total; +} + +void MatcherCases() { + std::vector userData(12); + for (std::uint32_t word = 0; word < userData.size(); ++word) userData[word] = 0x1000u + word; + ShaderRecompiler::ShaderComputeStageInfo compute{{8, 8, 1}, 0u, {true, true, true}, false, 2u, {0, 0, 0}}; + const std::array groups{8, 4, 1}; + std::vector code(ClearKernel.begin(), ClearKernel.end()); + code.insert(code.end(), {0u, 0u, 0u, 0u}); + + const auto clear = Graphics::MatchStorageImageClear(code, userData, compute, groups); + Require(clear.has_value(), "the clear kernel was not recognized"); + Require(clear->width == 64u && clear->height == 32u, "the clear covers " + std::to_string(clear->width) + " x " + std::to_string(clear->height) + " texels, expected 64 x 32"); + for (std::uint32_t word = 0; word < 8u; ++word) Require(clear->image[word] == 0x1000u + word, "the clear's T# is not user data 0-7"); + for (std::uint32_t word = 0; word < 4u; ++word) Require(clear->color[word] == 0x1008u + word, "the clear's colour V# is not user data 8-11"); + + for (std::size_t word = 0; word < ClearKernel.size(); ++word) { + auto changed = code; + changed[word] ^= 0x00000100u; + Require(!Graphics::MatchStorageImageClear(changed, userData, compute, groups), "a kernel with word " + std::to_string(word) + " changed was recognized"); + } + Require(!Graphics::MatchStorageImageClear(std::span(code).first(ClearKernel.size() - 1), userData, compute, groups), "a truncated kernel was recognized"); + Require(Graphics::MatchStorageImageClear(std::span(code).first(ClearKernel.size()), userData, compute, groups).has_value(), "the kernel alone, without trailing words, was not recognized"); + + auto shortUserData = userData; + shortUserData.pop_back(); + Require(!Graphics::MatchStorageImageClear(code, shortUserData, compute, groups), "eleven user registers were accepted (s12 would be a user register, not the group id)"); + auto longUserData = userData; + longUserData.push_back(0u); + Require(!Graphics::MatchStorageImageClear(code, longUserData, compute, groups), "thirteen user registers were accepted (s12 would be a user register, not the group id)"); + + auto wide = compute; + wide.numThreads = {8, 8, 2}; + Require(!Graphics::MatchStorageImageClear(code, userData, wide, groups), "8 x 8 x 2 threads were accepted"); + wide.numThreads = {16, 4, 1}; + Require(!Graphics::MatchStorageImageClear(code, userData, wide, groups), "16 x 4 threads were accepted"); + for (std::size_t axis = 0; axis < 3u; ++axis) { + auto missing = compute; + missing.groupIdEnable[axis] = false; + Require(!Graphics::MatchStorageImageClear(code, userData, missing, groups), "a dispatch without the group id of axis " + std::to_string(axis) + " was accepted"); + } + auto oneComponent = compute; + oneComponent.threadIdComponentCount = 1u; + Require(!Graphics::MatchStorageImageClear(code, userData, oneComponent, groups), "a dispatch with only the x thread id was accepted"); + + Require(!Graphics::MatchStorageImageClear(code, userData, compute, {8, 4, 2}), "two layers of groups were accepted"); + Require(!Graphics::MatchStorageImageClear(code, userData, compute, {0, 4, 1}), "zero groups along x were accepted"); + Require(!Graphics::MatchStorageImageClear(code, userData, compute, {8, 0, 1}), "zero groups along y were accepted"); + + auto partial = compute; + partial.partialThreads = {60, 30, 1}; + const auto cut = Graphics::MatchStorageImageClear(code, userData, partial, groups); + Require(cut.has_value() && cut->width == 60u && cut->height == 30u, "a dispatch of 60 x 30 threads does not cover 60 x 30 texels"); + +} + +std::vector DepthImageTexels(AgcDriver::VulkanDevice& device, const GuestBlock& plane, std::uint32_t format, std::uint32_t texelBytes) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto words = TextureDescriptor(plane.Address(), format); + const auto resource = Graphics::DecodeTextureResource(words); + std::shared_ptr mirror; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + mirror = Graphics::DepthSurfaceStorage(context, words, resource, 0); + } + Require(mirror != nullptr, "the plane has no mirror"); + mirror->MarkDirty(); + const std::size_t pitch = std::max(Width, 256u / texelBytes); + Graphics::StorageTexture::FlushPending(plane.Address(), pitch * Height * texelBytes, nullptr, "test"); + device.WaitIdle(); + std::vector texels(static_cast(Width) * Height); + for (std::size_t y = 0; y < Height; ++y) { + for (std::size_t x = 0; x < Width; ++x) { + const auto at = (y * pitch + x) * texelBytes; + if (texelBytes == 2u) { + std::uint16_t value = 0; + std::memcpy(&value, plane.Data() + at, sizeof(value)); + texels[y * Width + x] = value; + } else { + std::memcpy(&texels[y * Width + x], plane.Data() + at, sizeof(std::uint32_t)); + } + } + } + return texels; +} + +void RequireDepthTexels(AgcDriver::VulkanDevice& device, const GuestBlock& plane, std::uint32_t format, std::uint32_t texelBytes, std::uint32_t expected, const std::string& label) { + const auto texels = DepthImageTexels(device, plane, format, texelBytes); + for (std::size_t texel = 0; texel < texels.size(); ++texel) Require(texels[texel] == expected, label + "depth image texel " + std::to_string(texel) + " holds " + std::to_string(texels[texel]) + ", expected " + std::to_string(expected)); +} + +Graphics::DepthClearStatistics ClearSequence(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const auto drawLess = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}); }; + const auto drawGreater = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}); }; + const auto drawEqual = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_EQUAL}); }; + + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + Require(drawLess() == 0u, label + "the plane does not hold the 0.5 the producer wrote"); + Require(drawEqual() == Covered, label + "the plane does not hold exactly 0.5"); + + SetColor(color, 1.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawLess() == Covered, label + "after a clear to 1.0 the depth test 0.5 < 1.0 did not pass everywhere"); + Require(drawGreater() == 0u, label + "after a clear to 1.0 a texel passed 0.5 > depth"); + + SetColor(color, 0.25f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawLess() == 0u, label + "after a clear to 0.25 a texel passed 0.5 < depth"); + Require(drawGreater() == Covered, label + "after a clear to 0.25 the depth test 0.5 > 0.25 did not pass everywhere"); + + SetColor(color, 0.5f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawEqual() == Covered, label + "after a clear to 0.5 the plane does not hold exactly 0.5"); + + SetColor(color, 0.75f); + RunClear(device, code, plane, color, Format32Float, 4, 4); + Require(drawGreater() == 0u, label + "after a clear of the left half to 0.75 a texel passed 0.5 > depth"); + Require(drawEqual() == Covered / 2u, label + "after a clear of the left half the right half does not keep 0.5"); + Require(drawLess() == Covered / 2u, label + "after a clear of the left half the left half does not hold 0.75"); + + SetColor(color, 2.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawEqual() == 0u, label + "after a clear to 2.0 a texel still holds exactly 0.5"); + + SetColor(color, 0.125f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawGreater() == Covered, label + "after the last full clear the depth test 0.5 > 0.125 did not pass everywhere"); + Require(drawLess() == 0u, label + "after the last full clear a texel passed 0.5 < depth"); + + Graphics::StorageTexture::FlushPending(plane.Address(), Width * Height * sizeof(float), nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t texel = 0; texel < Width * Height; ++texel) { + float value = 0.0f; + std::memcpy(&value, plane.Data() + texel * sizeof(float), sizeof(value)); + Require(value == 0.125f, label + "guest depth texel " + std::to_string(texel) + " is " + std::to_string(value) + " after the flush, expected 0.125"); + } + RequireDepthTexels(device, plane, Format32Float, 4, std::bit_cast(0.125f), label); + return Difference(Graphics::DepthClearCounts(), before); +} + +void IntegerViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "integer view, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + const std::array bits{std::bit_cast(0.25f), 0u, 0u, 0u}; + std::memcpy(color.Data(), bits.data(), sizeof(bits)); + AgcDriver::GuestMemory::MarkWritten(color.Address(), sizeof(bits)); + RunClear(device, code, plane, color, Format32Uint, 8, 4, TileZ64KBX); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}) == Covered, label + "after a clear to the bits of 0.25 the depth test 0.5 > 0.25 did not pass everywhere"); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == 0u, label + "after a clear to the bits of 0.25 a texel passed 0.5 < depth"); + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) Require(difference.matched == 0u && difference.clears == 0u, label + "the lever counted with the switch off"); + else Require(difference.clears == 1u, label + "the clear was not taken (" + std::to_string(difference.clears) + " clears, " + std::to_string(Declined(difference)) + " declined)"); +} + +void DriverMatchCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "driver submit, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + SetColor(color, 1.0f); + DispatchClear(code, plane, color, Format32Float, 8, 4); + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) Require(difference.matched == 0u && difference.taken == 0u, label + "the driver's dispatch reached the lever with the switch off"); + else Require(difference.matched == 1u && difference.taken == 1u, label + "the driver's dispatch matched " + std::to_string(difference.matched) + " and took " + std::to_string(difference.taken) + " clears (declined " + std::to_string(Declined(difference)) + "), expected 1 and 1"); +} + +void SixteenBitCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "16-bit plane, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D16_UNORM}; + const auto drawLess = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}); }; + const auto drawGreater = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}); }; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + + SetColor(color, 1.0f); + try { + RunClear(device, code, plane, color, Format16UNorm, 8, 4); + } catch (const std::exception& error) { + if (std::string(error.what()).find("unsupported format") == std::string::npos) throw; + std::printf("16-bit plane skipped: %s\n", error.what()); + return; + } + Require(drawLess() == Covered, label + "after a clear to 1.0 the depth test 0.5 < 1.0 did not pass everywhere"); + Require(drawGreater() == 0u, label + "after a clear to 1.0 a texel passed 0.5 > depth"); + RequireDepthTexels(device, plane, Format16UNorm, 2, 0xffffu, label + "clear to 1.0: "); + + SetColor(color, 0.0f); + RunClear(device, code, plane, color, Format16UNorm, 8, 4); + Require(drawGreater() == Covered, label + "after a clear to 0.0 the depth test 0.5 > 0.0 did not pass everywhere"); + Require(drawLess() == 0u, label + "after a clear to 0.0 a texel passed 0.5 < depth"); + RequireDepthTexels(device, plane, Format16UNorm, 2, 0u, label + "clear to 0.0: "); + + SetColor(color, 0.5f); + RunClear(device, code, plane, color, Format16UNorm, 8, 4); + + auto& rawPlane = NewBlock(); + const Surface rawSurface{rawPlane.Address(), 0, 0, false, VK_FORMAT_D16_UNORM}; + const auto rawLess = [&] { return Draw(device, shaders, rawSurface, {.depthCompare = VK_COMPARE_OP_LESS}); }; + const auto rawGreater = [&] { return Draw(device, shaders, rawSurface, {.depthCompare = VK_COMPARE_OP_GREATER}); }; + Require(Draw(device, shaders, rawSurface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer of the uint plane did not cover it"); + SetRaw(color, 0x4000u); + RunClear(device, code, rawPlane, color, Format16Uint, 8, 4, TileZ64KBX); + Require(rawGreater() == Covered, label + "after a clear to the raw value 0x4000 the depth test 0.5 > 0.25 did not pass everywhere"); + Require(rawLess() == 0u, label + "after a clear to the raw value 0x4000 a texel passed 0.5 < depth"); + + SetRaw(color, 0xffffu); + RunClear(device, code, rawPlane, color, Format16Uint, 8, 4, TileZ64KBX); + Require(rawLess() == Covered, label + "after a clear to the raw value 0xffff the depth test 0.5 < 1.0 did not pass everywhere"); + Require(rawGreater() == 0u, label + "after a clear to the raw value 0xffff a texel passed 0.5 > depth"); + + SetRaw(color, 0u); + RunClear(device, code, rawPlane, color, Format16Uint, 8, 4, TileZ64KBX); + Require(rawGreater() == Covered, label + "after a clear to the raw value 0 the depth test 0.5 > 0.0 did not pass everywhere"); + Require(rawLess() == 0u, label + "after a clear to the raw value 0 a texel passed 0.5 < depth"); + + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) { + Require(difference.matched == 0u && difference.clears == 0u, label + "the lever counted with the switch off"); + return; + } + Require(difference.clears == 4u, label + "the lever took " + std::to_string(difference.clears) + " clears, expected 4 (unorm 1.0 and 0.0, raw 0xffff and 0)"); + Require(difference.declined[2] >= 2u, label + "the unorm clear to 0.5 and the raw clear to 0x4000 were not declined for their value (" + std::to_string(difference.declined[2]) + ")"); + Require(difference.bytesSkipped == difference.clears * 2u * Width * Height * sizeof(std::uint16_t), label + "the copies left out add up to " + std::to_string(difference.bytesSkipped) + " bytes"); +} + +void StencilCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "depth and stencil, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& stencilPlane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), stencilPlane.Address(), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + const VkStencilOpState equalFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, 5u}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, label + "the depth and stencil producer did not cover the surface"); + SetColor(color, 1.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == Covered, label + "after a clear to 1.0 the depth test 0.5 < 1.0 did not pass everywhere"); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = equalFive}) == Covered, label + "the depth clear changed the stencil values"); + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) Require(difference.matched == 0u && difference.clears == 0u, label + "the lever counted with the switch off"); + else Require(difference.clears == 1u, label + "the clear was not taken (" + std::to_string(difference.clears) + " clears, " + std::to_string(Declined(difference)) + " declined)"); +} + +} + +int main() { + int failures = 0; + const auto run = [&](const char* name, auto&& body) { + try { + body(); + } catch (const std::exception& error) { + std::cerr << name << ": " << error.what() << '\n'; + ++failures; + } + }; + try { + run("matcher", [] { MatcherCases(); }); + const auto device = OpenVulkanTestDevice(); + if (!device) return failures != 0 ? 1 : VulkanTestSkipped; + const auto shaders = Compile(*device); + + run("switch off", [&] { + const auto off = ClearSequence(*device, shaders, 0); + Require(off.matched == 0u && off.taken == 0u && off.clears == 0u && off.copiesSkipped == 0u, "the lever counted with the switch off"); + }); + + run("lever", [&] { + const auto on = ClearSequence(*device, shaders, 1); + Require(on.matched >= 6u, "the lever matched " + std::to_string(on.matched) + " clear dispatches, expected 6"); + Require(on.clears == 4u, "the lever took " + std::to_string(on.clears) + " clears, expected 4 (1.0, 0.25, 0.5, 0.125)"); + Require(on.copiesSkipped >= on.clears, "fewer copy-ins were left out than clears taken"); + Require(on.bytesSkipped == on.clears * 2u * Width * Height * sizeof(float), "the copies left out add up to " + std::to_string(on.bytesSkipped) + " bytes"); + Require(on.declined[2] >= 1u, "the clear to 2.0 was not declined for its value (" + std::to_string(on.declined[2]) + ")"); + Require(on.declined[5] >= 1u, "the clear of half the plane was not declined for its coverage (" + std::to_string(on.declined[5]) + ")"); + }); + + Graphics::SetDepthPlaneClearForTest(true); + + for (const int mode : {0, 1}) run(mode == 0 ? "integer view, switch off" : "integer view, lever", [&] { IntegerViewCase(*device, shaders, mode); }); + for (const int mode : {0, 1}) run(mode == 0 ? "16-bit plane, switch off" : "16-bit plane, lever", [&] { SixteenBitCase(*device, shaders, mode); }); + for (const int mode : {0, 1}) run(mode == 0 ? "depth and stencil, switch off" : "depth and stencil, lever", [&] { StencilCase(*device, shaders, mode); }); + for (const int mode : {0, 1}) run(mode == 0 ? "driver submit, switch off" : "driver submit, lever", [&] { DriverMatchCase(*device, shaders, mode); }); + Graphics::SetDepthPlaneClearForTest(true); + LibcRunShutdown_nid_postfix(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } + if (failures != 0) return 1; + std::puts("depth clear tests passed"); + return 0; +} 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/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index f43484e980..33c6cfdb6b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -50,6 +50,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Format16Unorm = 7; constexpr std::uint32_t Format16Uint = 11; +constexpr std::uint32_t Format32Uint = 20; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t TypeCube = 11; @@ -68,6 +69,7 @@ constexpr std::size_t FirstSliceOffset = 0xe0000; constexpr std::size_t PaddedCubeOffset = 0x120000; constexpr std::size_t SmallCubeOffset = 0x180000; constexpr std::size_t LargeCubeOffset = 0x200000; +constexpr std::size_t GuestDepthBitsOffset = 0x480000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; constexpr std::uint32_t DrawnD16Bits = 13107; @@ -324,6 +326,18 @@ void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, Expect(output, 0x1234u, "D16 depth-bits array from base slice 1"); } +void GuestDepthBitsCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + constexpr std::array values{0x00000001u, 0x007fffffu, 0x80000000u, 0x80000001u, 0x7f800001u, 0x7fc00001u, 0xffffffffu, 0x3e800000u, 0x12345678u}; + for (std::size_t index = 0; index < values.size(); ++index) { + auto* base = block.At(GuestDepthBitsOffset + index * SliceBytes); + FillSlice(base, SliceBytes, values[index], 4u); + Load(device, base, {Format32Uint, 128, 128, Type2D, 0, Swizzle64KbZX}, 0, output, false, static_cast(index * 13u)); + char what[96]; + std::snprintf(what, sizeof(what), "32-bit depth-bits read of guest memory holding 0x%08x", values[index]); + Expect(output, values[index], what); + } +} + void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { auto* one = block.At(OneSliceOffset); DrawDepth(device, reinterpret_cast(one), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); @@ -431,6 +445,7 @@ int main() { IntegerCase(*device, block, output); DepthBitsArrayCase(*device, block, output); DepthBitsBaseSliceCase(*device, block, output); + GuestDepthBitsCase(*device, block, output); FirstSliceCase(*device, block, output); PaddedCubeCase(*device, block, output); SmallD16CubeCase(*device, block, output); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index f83e70edb5..3ab804d307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -109,6 +109,7 @@ public: if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { @@ -199,7 +200,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/FlatStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp index f249083ba2..2d6da41fd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp @@ -31,7 +31,9 @@ constexpr std::uint32_t MaskedThreads = 16; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t CheckedBytes = 0x2000; constexpr std::uint8_t Fill = 0xcd; -#ifdef __linux__ +#if defined(__linux__) && defined(ANYPS5_ENABLE_ASAN) +constexpr std::array GuestWindowAddresses{0x60000000, 0x200000000000}; +#elif defined(__linux__) constexpr std::array GuestWindowAddresses{0x80000000000, 0x200000000000}; #endif 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 index 704f0fa843..12eb8b6c06 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -22,7 +23,8 @@ alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t ArgumentsOffset = 4; -alignas(BlockBytes) std::array Block{}; +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{}; @@ -71,6 +73,7 @@ void Program(std::vector& words, const void* code, std::initializ 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(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp index f415d1e5e8..5a4da94b89 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -31,9 +31,14 @@ 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; +#ifdef ANYPS5_ENABLE_ASAN +constexpr std::size_t FiberStackLimit = 16 * 1024; +#else constexpr std::size_t FiberStackLimit = 8 * 1024; +#endif constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull; alignas(16) std::array FiberContext; @@ -84,10 +89,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 +126,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/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp new file mode 100644 index 0000000000..9e9e22d814 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -0,0 +1,259 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +namespace { + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t FillBytes = 4096; +constexpr std::array Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t Address(const void* pointer) { + return reinterpret_cast(pointer); +} + +bool Imported(const AgcDriver::VulkanDevice& device, const void* pointer, std::size_t bytes = BlockBytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, Address(pointer), bytes); +} + +void* Map(std::size_t bytes = BlockBytes) { + auto* result = mmap_nid_postfix(nullptr, bytes, 3, 0x1002, -1, 0); + Require(result != reinterpret_cast(~std::uintptr_t{0}), "guest mmap failed"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(result); + Require(range.readable && range.writable && !range.gpu, "guest mmap did not register CPU-only read/write access"); + } + std::memset(result, 0, bytes); + return result; +} + +void CheckPattern(const void* pointer) { + const auto* data = static_cast(pointer); + for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern)) + Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill"); +} + +void Fill(AgcDriver::VulkanDevice& device, void* pointer) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation"); + Require(Imported(device, pointer), "GPU fill did not create a host import"); +} + +void CheckMappings(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection refresh failed"); + Require(!Imported(device, mapping), "host import survived guest writable protection refresh"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "guest read-only protection failed"); + Require(!Imported(device, mapping), "host import survived guest protection change"); + CheckPattern(mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 0) == 0, "guest inaccessible protection failed"); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection restore failed"); + CheckPattern(mapping); + Fill(device, mapping); + void* replaced = mapping; + Require(sceKernelMapFlexibleMemory(&replaced, BlockBytes, 0x33, 0x10) == 0 && replaced == mapping, "guest fixed mapping replacement failed"); + Require(!Imported(device, mapping), "host import survived guest fixed mapping replacement"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(mapping); + Require(range.readable && range.writable && range.gpu, "guest fixed mapping did not register its requested CPU and GPU access"); + } + const auto* data = static_cast(mapping); + for (std::size_t offset = 0; offset < BlockBytes; ++offset) + Require(data[offset] == 0, "GPU work reached replacement guest pages"); + Fill(device, mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest munmap failed"); + Require(!Imported(device, mapping), "host import survived guest munmap"); + + mapping = Map(3 * BlockBytes); + Fill(device, mapping); + auto* middle = static_cast(mapping) + BlockBytes; + Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed"); + Require(!Imported(device, mapping), "whole host import survived partial protection change"); + CheckPattern(mapping); + Fill(device, mapping); + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); + Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap"); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); + Require(!Imported(device, mapping), "prefix host import survived guest munmap"); + Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed"); +} + +void CheckHeap(AgcDriver::VulkanDevice& device) { + const std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + auto* allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest aligned allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + auto* replacement = realloc_nid_postfix(allocation, 2 * BlockBytes); + Require(replacement != nullptr, "guest realloc failed"); + Require(!Imported(device, allocation), "host import survived guest realloc"); + CheckPattern(replacement); + free_nid_postfix(replacement); + for (int iteration = 0; iteration < 3; ++iteration) { + allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest heap reuse allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + free_nid_postfix(allocation); + Require(!Imported(device, allocation), "host import survived guest free or heap cache eviction"); + } +} + +void CheckLockedMutation(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "writable protection refresh under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection refresh retained the import"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "protection change under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection change retained the import"); + CheckPattern(mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "unmap under the GPU lock did not finish"); + } + + mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + auto lease = std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "Vulkan device has no active recorder"); + recorder->Keep(std::move(lease)); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "completed batch kept its lease until the outer GPU lock was released"); + } +} + +void CheckCopy(AgcDriver::VulkanDevice& device) { + auto* source = Map(); + auto* destination = Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + const auto copied = device.CopyBuffer(Address(destination), Address(source), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + Require(copied.path == 1, "guest source was not read by a Vulkan host-import transfer"); + Require(Imported(device, source) && Imported(device, destination), "GPU copy did not import both guest ranges"); + } + Require(mprotect_nid_postfix(source, BlockBytes, 3) == 0, "copied source protection refresh failed"); + Require(!Imported(device, source), "GPU copy source remained imported after guest protection refresh"); + CheckPattern(destination); + Require(Imported(device, destination), "source retirement unnecessarily retired the copy destination"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + const auto copied = device.CopyBuffer(Address(destination), Address(source), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + Require(copied.path == 1, "second source transfer did not use host imports"); + } + Require(munmap_nid_postfix(source, BlockBytes) == 0, "copied source unmap failed"); + Require(!Imported(device, source), "GPU copy source remained imported after guest unmap"); + CheckPattern(destination); + Require(Imported(device, destination), "source unmap retired the unrelated copy destination"); + Require(munmap_nid_postfix(destination, BlockBytes) == 0, "copy destination unmap failed"); + Require(!Imported(device, destination), "GPU copy destination remained imported after guest unmap"); +} + +void CheckDirectRelease(AgcDriver::VulkanDevice& device) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, BlockBytes, BlockBytes, 0, &physical) == 0, "guest direct allocation failed"); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, BlockBytes, 0x33, 0, physical, BlockBytes) == 0, "first direct view mapping failed"); + Require(sceKernelMapDirectMemory(&second, BlockBytes, 0x33, 0, physical, BlockBytes) == 0 && second != first, "second direct view mapping failed"); + { + GuestAllocations::Mutation mutation; + const auto firstRange = mutation.Find(first); + const auto secondRange = mutation.Find(second); + Require(firstRange.readable && firstRange.writable && firstRange.gpu && secondRange.readable && secondRange.writable && secondRange.gpu, "direct views did not register their requested CPU and GPU access"); + } + std::memset(first, 0, BlockBytes); + static_cast(first)[BlockBytes - 1] = 0x5a; + Require(static_cast(second)[BlockBytes - 1] == 0x5a, "direct views do not share their physical backing"); + auto* destination = Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, first); + Require(device.FillBuffer(Address(second) + FillBytes, FillBytes, Pattern), "second direct view was not imported for its GPU fill"); + const auto firstCopy = device.CopyBuffer(Address(destination), Address(first), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + const auto secondCopy = device.CopyBuffer(Address(destination) + FillBytes, Address(second) + FillBytes, FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + Require(firstCopy.path == 1 && secondCopy.path == 1, "direct views were not read through GPU import transfers"); + Require(Imported(device, first) && Imported(device, second), "both direct view imports were not retained before release"); + } + Require(sceKernelReleaseDirectMemory(physical, BlockBytes) == 0, "guest direct release failed"); + Require(!Imported(device, first) && !Imported(device, second), "direct release retained an imported virtual view"); + Require(!AgcDriver::Graphics::RegisteredReadableCovers(Address(first), BlockBytes) && !AgcDriver::Graphics::RegisteredReadableCovers(Address(second), BlockBytes), "direct release left a virtual view registered"); + CheckPattern(destination); + CheckPattern(static_cast(destination) + FillBytes); + Require(Imported(device, destination), "direct release retired the unrelated copy destination"); + Require(munmap_nid_postfix(destination, BlockBytes) == 0, "direct release copy destination unmap failed"); + Require(!Imported(device, destination), "direct release copy destination remained imported after unmap"); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc <= 2, "expected mappings, heap, locked, copy or direct test mode"); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!GuestMemoryLifetimeHasHostImports(device->Device())) return VulkanTestSkipped; + const std::string_view mode = argc == 2 ? argv[1] : "mappings"; + if (mode == "mappings") CheckMappings(*device); + else if (mode == "heap") CheckHeap(*device); + else if (mode == "locked") CheckLockedMutation(*device); + else if (mode == "copy") CheckCopy(*device); + else if (mode == "direct") CheckDirectRelease(*device); + else throw std::runtime_error("unknown guest memory lifetime test mode"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device->WaitIdle(); + } + device.reset(); + std::puts("guest memory host import lifetime passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "guest memory host import lifetime: %s\n", error.what()); + return 1; + } +} 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..e2f8d2dc66 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, MipNone, Aniso2x}, "min, 2x anisotropic bilinear, no mip"); + Run(*device, {ReductionMax, FilterAnisoBilinear, MipNone, Aniso2x}, "max, 2x anisotropic bilinear, no 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/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp index ec759a3f7c..eafabe0d7c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -156,7 +156,26 @@ void Fill(std::uint32_t format) { } } -void Run(AgcDriver::VulkanDevice& device, std::span code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups, bool useCache = false) { +void RequireInitializedTable(std::span words, const std::string& what) { + std::vector tables; + std::size_t stores = 0; + std::size_t initialized = 0; + for (std::size_t at = 5; at < words.size();) { + const auto count = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + Require(count != 0u && at + count <= words.size(), what + ": malformed SPIR-V"); + const auto operands = words.subspan(at + 1u, count - 1u); + const auto table = [&](std::uint32_t id) { return std::find(tables.begin(), tables.end(), id) != tables.end(); }; + if (opcode == 44u && operands.size() == 2u + 256u) tables.push_back(operands[1]); + if (opcode == 59u && operands.size() == 4u && table(operands[3])) ++initialized; + if (opcode == 62u && operands.size() >= 2u && table(operands[1])) ++stores; + at += count; + } + Require(tables.size() == 1u, what + ": the shader has " + std::to_string(tables.size()) + " sRGB tables, expected 1"); + Require(stores == 0u && initialized == 1u, what + ": the sRGB table is stored " + std::to_string(stores) + " times and initializes " + std::to_string(initialized) + " variables, expected no store and one variable"); +} + +std::vector Run(AgcDriver::VulkanDevice& device, std::span code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups, bool useCache = false) { Output.fill(0xdeadbeefu); std::vector userData(24, 0u); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); @@ -176,6 +195,7 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); + return result.spirv.Copy(); } void Verify(std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { @@ -194,7 +214,7 @@ void Verify(std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, co void CheckLoad(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { Fill(format); - Run(device, code, Texels.data(), format, swizzle, Groups); + RequireInitializedTable(Run(device, code, Texels.data(), format, swizzle, Groups), what); Verify(dmask, format, swizzle, what); } 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 43ff32af56..bbabab005a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -110,6 +110,20 @@ alignas(256) constexpr std::array TableWave32Code{ 0x01090401u, 0x7e120202u, 0x34101287u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array SelectedTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u, + 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, 0x34101286u, + 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array SelectedTableCutCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u, + 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0x7da400ffu, 0x00001000u, 0xf09c8f08u, 0x01090401u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + alignas(256) constexpr std::array DirectCode{ 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, @@ -184,12 +198,25 @@ 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, 0xbf810000u, }; +constexpr std::uint32_t SelectedStoreValue = 0xc0de5eedu; + +alignas(256) constexpr std::array SelectedStoreCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4000400u, 0xfa000060u, 0xf4200444u, 0x20000000u, + 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e0802ffu, SelectedStoreValue, 0xf0208f08u, + 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array PointerCode{ 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0x360200bfu, 0x7e020d01u, @@ -368,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); } @@ -934,6 +961,43 @@ void RunGuardedTableTests(AgcDriver::VulkanDevice& device) { SetClassRecords(); } +void RunSelectedRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + SetClassRecords(); + Keys = {}; + Srt[24] = 12u; + Outcome first; + for (const auto record : {2u, 0u, 1u}) { + Keys[3] = record; + const auto selected = Run(device, SelectedTableCode, 1, Lanes, 64); + Require(!Faulted(selected) && selected.imageTableCount == 1u, "image table selected record: record " + std::to_string(record) + " faulted or bound other entries:\n" + selected.log); + const auto* words = Table.data() + record * (Stride / 4u); + const auto control = RunDirect(device, {words[0], words[1], words[2], words[3]}, {words[4], words[5], words[6], words[7]}); + Require(Group(selected, 0) == Group(control, 0), "image table selected record: record " + std::to_string(record) + " differs from the direct binding of the same words"); + if (record == 2u) first = selected; + Require(selected.artifactId == first.artifactId && selected.variantId == first.variantId, "image table selected record: another key changed the module"); + } + + Keys[3] = 1000u; + const auto beyond = Run(device, SelectedTableCode, 1, Lanes, 64); + Require(!Faulted(beyond) && Zeros(beyond), "image table selected record: a key past the table faulted or did not sample zeros:\n" + beyond.log); + + Keys[3] = 7u; + const auto cut = Run(device, SelectedTableCutCode, 1, Lanes, 64); + Require(!Faulted(cut), "image table selected record: an invalid record selected with EXEC clear faulted:\n" + cut.log); + const auto invalid = Run(device, SelectedTableCode, 1, Lanes, 64); + const auto tableAddress = static_cast(reinterpret_cast(Table.data())) + 7u * Stride + 16u; + char address[32]; + std::snprintf(address, sizeof(address), "0x%llx", static_cast(tableAddress)); + Require(Faulted(invalid) && invalid.log.find("not an image") != std::string::npos && invalid.log.find(address) != std::string::npos && invalid.log.find("00001234") != std::string::npos && Zeros(invalid), "image table selected record: an invalid record selected with EXEC set did not report its address, words and reason:\n" + invalid.log); + + Keys[3] = 0u; + const auto aliased = Run(device, SelectedTableCode, 1, Lanes, 64, true); + Require(Faulted(aliased) && aliased.log.find("is written by its own dispatch") != std::string::npos, "image table selected record: a dispatch storing over its selected record did not fault on use:\n" + aliased.log); + SetClassRecords(); + Srt[24] = 0u; +} + template void RequireModeEntry(AgcDriver::VulkanDevice& device, const std::array& tableCode, const std::array& directCode, std::uint32_t record, std::span image, const std::string& what) { Keys = {}; @@ -988,15 +1052,61 @@ 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) { + alignas(256) static std::array storeTexels{}; + 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); } + } registered(storeTexels.data(), sizeof(storeTexels)); + for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) storeTexels[texel] = 0x5000u + texel; + const auto saved = storeTexels; + SetClassRecords(); + SetRecord(9, PointClamp, Image(storeTexels.data(), Format32UInt)); + Keys = {}; + Keys[3] = 9u; + Srt[24] = 12u; + const auto stored = Run(device, SelectedStoreCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(storeTexels.data()), sizeof(storeTexels), nullptr, "test"); + device.WaitIdle(); + Require(!Faulted(stored) && stored.imageTableCount == 0u, "image table selected store: an image_store through a T# whose record the CPU evaluates faulted or was bound through an image table:\n" + stored.log); + for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) { + const auto expected = texel < Lanes ? SelectedStoreValue : saved[texel]; + Require(storeTexels[texel] == expected, "image table selected store: texel " + std::to_string(texel) + " holds " + std::to_string(storeTexels[texel]) + " after an image_store through a T# whose record the CPU evaluates, expected " + std::to_string(expected)); + } } void RunPointerTests(AgcDriver::VulkanDevice& device) { @@ -1325,8 +1435,10 @@ int main() { RunReadTests(*device); RunLoopKeyTests(*device); RunGuardedTableTests(*device); + RunSelectedRecordTests(*device); RunModeTests(*device); RunStoreTests(*device); + RunSelectedStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); RunSplitRecordTests(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp index 52c5728108..4c3e66ac8f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp @@ -27,6 +27,7 @@ constexpr std::uint32_t Height = 24; constexpr std::uint32_t Depth = 20; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t TileS4KBX = 0x15; constexpr std::uint32_t TileZ64KBX = 0x18; constexpr std::uint32_t TileS64KBX = 0x19; constexpr std::uint32_t TileD64KBX = 0x1a; @@ -79,6 +80,7 @@ void FillTexels(std::uint32_t tileMode) { Require(geometry.guestBytes <= Texels.size(), "volume does not fit the test allocation"); const auto* equation = FindTextureSwizzleEquation(geometry.thick ? 0x100u | XorSwizzleMode(descriptor.tileMode) : XorSwizzleMode(descriptor.tileMode), 4u); Require(equation != nullptr, "volume swizzle equation is missing"); + const auto blockBytes = ThinBlockLayout(descriptor.tileMode, 4u)[0]; const auto& mip = geometry.mips.at(0); const auto block = geometry.thick ? ThickBlockExtent(descriptor.tileMode, 4u) : std::array{ThinBlockLayout(descriptor.tileMode, 4u)[1], ThinBlockLayout(descriptor.tileMode, 4u)[2], 1u}; for (std::uint32_t z = 0; z < Depth; ++z) { @@ -86,7 +88,7 @@ void FillTexels(std::uint32_t tileMode) { for (std::uint32_t x = 0; x < Width; ++x) { auto offset = geometry.GuestLayerOffset(z) + mip.tiledOffset; if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, z); - else offset += (static_cast(y / block[1]) * mip.blocksPerRow + x / block[0]) * 65536u + EquationOffset(*equation, x, y, z); + else offset += (static_cast(y / block[1]) * mip.blocksPerRow + x / block[0]) * blockBytes + EquationOffset(*equation, x, y, z); const auto texel = Texel(x, y, z); std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(offset)); } @@ -145,6 +147,7 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillInput(); + Run(*device, TileS4KBX, "thick SW_4KB_S_X volume"); Run(*device, TileS64KBX, "thick SW_64KB_S_X volume"); Run(*device, TileD64KBX, "thick SW_64KB_D_X volume"); Run(*device, TileZ64KBX, "thin SW_64KB_Z_X volume"); 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/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp new file mode 100644 index 0000000000..c149aa41a9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -0,0 +1,254 @@ +#include "VulkanTestDevice.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" void AgcDriverShutdown_nid_postfix(); + +struct MemorySizeQueryParams { + std::uint32_t version; + std::uint32_t workers; + std::uint32_t sequences; + std::uint32_t invocations; + std::uint32_t dispatches; + std::uint32_t syncPoints; + std::uint32_t waits; + std::uint32_t computeSequences; + std::uint64_t computeQueueMask; +}; + +struct ConfigParams { + std::uint32_t version; + std::uint32_t threadFlags; + std::uint32_t stackSize; + std::uint32_t priority; + const char* threadName; + std::uint32_t spinMicroseconds; + std::uint32_t computeAffinity; + std::uint32_t computePriority; + std::uint32_t reserved; +}; + +using JobEntry = void (APS5_VABI*)(void*, std::uint32_t); + +struct Sequence { + APS5_VABI virtual int Dispatch(JobEntry, void*, std::uint32_t, const char*) = 0; + APS5_VABI virtual int SyncPoint() = 0; + APS5_VABI virtual int Wait() = 0; + APS5_VABI virtual void CompleteDestructor() = 0; + APS5_VABI virtual void DeletingDestructor() = 0; + APS5_VABI virtual int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) = 0; + APS5_VABI virtual int DispatchComputeIndirect(const void*, void*, const void*, const char*) = 0; +}; + +struct SequenceFactory { + APS5_VABI virtual int Create(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0; + APS5_VABI virtual int Destroy(Sequence*) = 0; + APS5_VABI virtual void CompleteDestructor() = 0; + APS5_VABI virtual void DeletingDestructor() = 0; + APS5_VABI virtual int CreateCompute(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0; + APS5_VABI virtual int DestroyCompute(Sequence*) = 0; +}; + +extern "C" { +void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*); +std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*); +int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, const ConfigParams*); +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void*); +SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t); +} + +#define Construct _ZN3sce3Job10JobManagerC1Ev +#define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE +#define Initialize _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE +#define Shutdown _ZN3sce3Job10JobManager8shutdownEv +#define Factories _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE + +namespace { + +constexpr int InvalidArgument = static_cast(0x81581000u); +constexpr int OutOfRange = static_cast(0x81580000u); +constexpr int NullArgument = static_cast(0x81580001u); +constexpr int NoNodes = static_cast(0x81580002u); +constexpr int LimitExceeded = static_cast(0x81580003u); +constexpr int NoneFree = static_cast(0x81580006u); +constexpr int Invalid = static_cast(0x81580007u); + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array Value{}; +alignas(256) std::array Table{}; +alignas(256) volatile std::uint32_t Output[4]{}; +alignas(256) std::array OutputDescriptor{}; +alignas(32) std::array IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0}; +alignas(16) std::array Specials{}; +alignas(16) std::array SpecialsWithoutDispatch{}; +std::array Registers{}; +alignas(16) std::array Shader{}; +alignas(16) std::array ShaderWithoutDispatch{}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +void BuildShader() { + Table = {Low(Value.data()), High(Value.data())}; + OutputDescriptor = {Low(Output), High(Output) & 0xffffu, sizeof(Output), 0x31016facu}; + const auto code = static_cast(reinterpret_cast(ReaderCode.data())); + Registers = {{{0x20c, static_cast(code >> 8u)}, {0x20d, static_cast(code >> 40u) & 0xffu}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4u << 1u}, + {0x242, Low(OutputDescriptor.data())}, {0x243, High(OutputDescriptor.data())}}}; + Specials[4] = 1; + const ShaderRegister* registers = Registers.data(); + for (auto* shader : {&Shader, &ShaderWithoutDispatch}) { + const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsWithoutDispatch.data(); + std::memcpy(shader->data() + 0x20, ®isters, sizeof(registers)); + std::memcpy(shader->data() + 0x28, &specials, sizeof(specials)); + (*shader)[0x5c] = static_cast(Registers.size()); + } +} + +std::uint64_t SizeFor(std::uint32_t computeSequences, std::uint64_t mask, bool limits) { + const MemorySizeQueryParams query{1, 2, 1, limits ? 100u : 0u, limits ? 8u : 0u, limits ? 4u : 0u, limits ? 2u : 0u, computeSequences, mask}; + return RequiredSize(&query); +} + +std::atomic Released{false}; +std::atomic SeenByJob{0}; + +void APS5_VABI BlockingJob(void*, std::uint32_t) { + while (!Released) std::this_thread::yield(); +} + +void APS5_VABI ReadOutputJob(void*, std::uint32_t) { + SeenByJob = Output[0]; +} + +void APS5_VABI Nothing(void*, std::uint32_t) { +} + +void SizeCase() { + Require(SizeFor(0, 0, false) == 0x10d0 && SizeFor(0, 0, true) == 0x14c0, "size without compute sequences"); + Require(SizeFor(1, 1, false) == 0x24fd8 && SizeFor(1, 1, true) == 0x253c8, "size with one compute sequence"); + Require(SizeFor(2, 3, false) == 0x48e20 && SizeFor(2, 3, true) == 0x49210, "size with two compute sequences"); + Require(SizeFor(3, 7, false) == 0x6cb68, "size with three compute sequences"); + Require(SizeFor(1, 3, true) == 0x491c8, "size with two queues for one compute sequence"); + Require(SizeFor(2, 1, true) == 0, "size with fewer queues than compute sequences"); +} + +void RunCase() { + const std::uint64_t size = SizeFor(1, 2, true); + void* memory = ::operator new((size + 255) & ~std::uint64_t{255}, std::align_val_t{256}); + Require(memory != nullptr, "allocation failed"); + alignas(64) static unsigned char manager[0x100]; + Construct(manager); + const ConfigParams badHandler{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x50, 0}; + const ConfigParams noAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0, 0x100, 0}; + const ConfigParams wideAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x2000, 0x100, 0}; + Require(Initialize(manager, memory, size, 2, 1, 1, 2, &badHandler) == InvalidArgument, "a handler priority below 0x100 was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, &noAffinity) == InvalidArgument, "an empty handler affinity was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, &wideAffinity) == InvalidArgument, "a handler affinity beyond 13 cores was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 0xffffffffu, 2, nullptr) == InvalidArgument, "a sequence count that wraps was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, nullptr) == 0, "initialize with one compute sequence failed"); + SequenceFactory* factory = Factories(manager, 0); + Sequence* sequence = nullptr; + Sequence* other = nullptr; + Require(factory->CreateCompute(&sequence, 100, 4, 4, 2, "compute") == 0, "the compute sequence was not created"); + Require(factory->CreateCompute(&other, 1, 1, 1, 1, "second") == NoneFree, "a second compute sequence was created"); + Require(factory->Create(&other, 1, 1, 1, 1, "cpu") == 0, "the CPU sequence was not created"); + Require(factory->DestroyCompute(other) == Invalid, "a CPU sequence was destroyed as a compute sequence"); + Require(factory->Destroy(sequence) == Invalid, "a compute sequence was destroyed as a CPU sequence"); + Require(factory->Destroy(other) == 0, "the CPU sequence was not destroyed"); + + Require(sequence->DispatchCompute(nullptr, Table.data(), 1, 1, 1, nullptr) == NullArgument, "a null shader was accepted"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 0, 1, 1, nullptr) == OutOfRange, "a zero group count was accepted"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, -1, nullptr) == OutOfRange, "a negative group count was accepted"); + Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), nullptr, nullptr) == NullArgument, "null indirect arguments were accepted"); + Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), reinterpret_cast(IndirectArguments.data()) + 4, nullptr) == NullArgument, "misaligned indirect arguments were accepted"); + + Value[0] = 0x11110001u; + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "direct") == 0, "the compute dispatch failed"); + Require(sequence->Wait() == 0, "the wait after a compute dispatch failed"); + Require(Output[0] == 0x11110001u, "the compute dispatch had not run when the wait returned: " + std::to_string(Output[0])); + + Value[0] = 0x22220002u; + Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), IndirectArguments.data(), nullptr) == 0, "the indirect compute dispatch failed"); + Require(sequence->Wait() == 0, "the wait after an indirect compute dispatch failed"); + Require(Output[0] == 0x22220002u, "the indirect compute dispatch had not run when the wait returned: " + std::to_string(Output[0])); + + Value[0] = 0x33330003u; + Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch of a shader without the dispatch bit failed"); + Require(sequence->Wait() == 0, "the wait after a shader without the dispatch bit failed"); + Require(Output[0] == 0x22220002u, "a shader without the dispatch bit ran"); + + Require(sequence->Dispatch(BlockingJob, nullptr, 1, "blocking") == 0, "the blocking job was not dispatched"); + Require(sequence->SyncPoint() == 0, "the sync point failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the compute dispatch behind a sync point failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Output[0] == 0x22220002u, "a compute dispatch ran before the CPU job ahead of its sync point finished"); + Released = true; + Require(sequence->SyncPoint() == 0, "the second sync point failed"); + Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, "reader") == 0, "the reading job was not dispatched"); + Require(sequence->Wait() == 0, "the wait after the ordered dispatches failed"); + Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run"); + Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); + + Value[0] = 0x44440004u; + Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed"); + Require(sequence->SyncPoint() == 0, "the sync point between compute dispatches failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "behind") == 0, "the compute dispatch behind a compute dispatch failed"); + Require(sequence->Wait() == 0, "the wait after chained compute dispatches failed"); + Require(Output[0] == 0x44440004u, "a compute dispatch behind a sync point and a compute dispatch did not run"); + + for (int index = 0; index < 4; ++index) Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "a compute dispatch within the limit failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == LimitExceeded, "a compute dispatch past the dispatch limit was accepted"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == Invalid, "a latched compute sequence accepted a dispatch"); + Require(factory->DestroyCompute(sequence) == 0, "the compute sequence was not destroyed"); + + Require(factory->Create(&other, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0, "the node counting sequence was not created"); + int accepted = 0; + int result = 0; + while (accepted < 0x3ff && (result = other->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted; + Require(result == NoNodes && accepted == 43, "the node pool of a compute configuration holds " + std::to_string(accepted) + " dispatches"); + Require(Shutdown(manager) == 0, "shutdown failed"); + ::operator delete(memory, std::align_val_t{256}); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + BuildShader(); + SizeCase(); + RunCase(); + AgcDriverShutdown_nid_postfix(); + std::cout << "job manager compute test passed\n"; + 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/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 20ab53b94c..5581d46dcc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -97,9 +97,9 @@ void BuildBands() { } } -AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume) { +AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume, ColorTileMode tileMode = ColorTileMode::RenderTarget) { AgcDriver::Graphics::ColorTarget color{reinterpret_cast(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; - color.tileMode = ColorTileMode::RenderTarget; + color.tileMode = tileMode; color.depth = Slices; color.depthSlice = FirstSlice; color.layers = Layers; @@ -239,19 +239,23 @@ std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equ } void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& color, const std::string& what, const std::array& bands) { + using namespace AgcDriver::Graphics; const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)); - Require(!geometry.thick, "a 64 KiB render-target volume is not stored as thin slices"); - const auto block = AgcDriver::Graphics::ThinBlockLayout(AgcDriver::Graphics::TextureTileMode::kR64KBX, 4); - Require(Width <= block[1] && Height <= block[2], "the volume slices span several tile blocks"); - const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(static_cast(ColorTileMode::RenderTarget), 4); - Require(equation != nullptr, "missing the 64 KiB R_X swizzle equation"); + Require(geometry.thick == (color.tileMode == ColorTileMode::S4KBX), "unexpected volume addressing family"); + const auto tileMode = ColorTextureTileMode(color.tileMode); + const auto thin = ThinBlockLayout(tileMode, 4); + const auto block = geometry.thick ? ThickBlockExtent(tileMode, 4) : std::array{thin[1], thin[2], 1u}; + const auto& mip = geometry.mips.at(0); + const auto* equation = FindTextureSwizzleEquation((geometry.thick ? 0x100u : 0u) | static_cast(color.tileMode), 4); + Require(equation != nullptr, "missing the volume's swizzle equation"); for (std::uint32_t slice = 0; slice < Slices; ++slice) { const auto* base = volume + geometry.GuestLayerOffset(slice); for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { const bool drawn = slice >= FirstSlice && x / BandWidth == slice - FirstSlice; const auto expected = drawn ? bands[slice - FirstSlice] : Kept; - const auto* texel = base + BlockOffset(*equation, x, y, slice); + const auto offset = (static_cast(y / block[1]) * mip.blocksPerRow + x / block[0]) * thin[0] + BlockOffset(*equation, x, y, slice); + const auto* texel = base + offset; for (std::uint32_t channel = 0; channel < 4; ++channel) { const auto actual = std::to_integer(texel[channel]); Require(actual == expected || (drawn && expected == 128 && actual == 127), what + ": slice " + std::to_string(slice) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (drawn ? " (the band of layer " + std::to_string(slice - FirstSlice) + ")" : slice < FirstSlice ? " (outside the color view)" : " (another layer's band)")); @@ -283,6 +287,10 @@ int main(int argc, char** argv) { const auto fieldColor = VolumeTarget(fieldVolume.Data()); Run(*device, fieldColor, false, LayerFieldPixelCode, true); Check(fieldVolume.Data(), fieldColor, "a pixel shader reading the render target index of its packed ancillary word", LayerFieldBands); + const GuestVolume thickVolume((VolumeTarget(nullptr, ColorTileMode::S4KBX).bytes + 65535u) & ~std::size_t{65535u}); + const auto thickColor = VolumeTarget(thickVolume.Data(), ColorTileMode::S4KBX); + Run(*device, thickColor, false, PixelCode, false); + Check(thickVolume.Data(), thickColor, "thick SW_4KB_S_X vertex draw and writeback", WhiteBands); if (vertexOnly) std::puts("layered volume vertex draw tests passed"); else if (target.mesh.has_value()) Run(*device, color, true, PixelCode, false); else std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); 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/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index 6528e8af9e..7c35530d9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -6,9 +6,13 @@ #include #include #include +#include #include +#include #include #include +#include +#include #include namespace { @@ -182,14 +186,41 @@ void Check(const char* name, std::uint64_t operations, std::uint32_t run) { } } -void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats) { +std::uint64_t WorkgroupBytes(std::span words) { + std::unordered_map> types; + std::unordered_map constants; + std::vector pointers; + for (std::size_t at = 5; at < words.size();) { + const auto count = words[at] >> 16u; + Require(count != 0u && count <= words.size() - at, "lds lock budget: malformed SPIR-V"); + const auto instruction = words.subspan(at, count); + const auto opcode = instruction[0] & 0xffffu; + if (opcode == spv::OpTypeInt || opcode == spv::OpTypeArray || opcode == spv::OpTypePointer) types[instruction[1]] = instruction; + if (opcode == spv::OpConstant) constants[instruction[2]] = instruction[3]; + if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]); + at += count; + } + std::uint64_t total = 0; + for (const auto pointer : pointers) { + const auto definition = types.at(pointer); + Require((definition[0] & 0xffffu) == spv::OpTypePointer, "lds lock budget: Workgroup storage has no pointer type"); + const auto array = types.at(definition[3]); + Require((array[0] & 0xffffu) == spv::OpTypeArray, "lds lock budget: unexpected Workgroup allocation shape"); + const auto element = types.at(array[2]); + Require((element[0] & 0xffffu) == spv::OpTypeInt && element[2] == 32u, "lds lock budget: Workgroup array is not dwords"); + total += static_cast(constants.at(array[3])) * sizeof(std::uint32_t); + } + return total; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional expectedDeviceDwords = std::nullopt) { 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}, LdsDwords, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, ldsDwords, {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}, @@ -198,20 +229,46 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); + if (expectedDeviceDwords) { + const auto bytes = WorkgroupBytes(result.spirv.Words()); + const auto limit = request.target.maxWorkgroupSharedMemoryBytes; + Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch"); + Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch"); + if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock"); + std::printf("lds lock budget: %s declarations verified; guest=%u; workgroup=%llu; device=%u\n", name, ldsDwords * 4u, static_cast(bytes), result.workgroupMemoryDwords); + } for (std::uint32_t run = 0; run < repeats; ++run) { Output.fill(0xdeadbeefu); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); Check(name, operations, run); } + if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name); +} + +void LockBudget(AgcDriver::VulkanDevice& device) { + const auto target = device.Target(); + const auto limit = target.maxWorkgroupSharedMemoryBytes; + std::printf("lds lock budget: robustBufferAccess2=%u; maxWorkgroupSharedMemoryBytes=%u\n", static_cast(target.robustBufferAccess2), limit); + if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled"); + Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); + const auto operations = static_cast(Threads) * Iterations; + Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u); + Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); + std::puts("lds lock budget tests passed"); } } -int main() { +int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--lock-budget") { + LockBudget(*device); + return 0; + } + Require(argc == 1, "lds mixed width atomics: unexpected argument"); Run(*device, UniformCode, "uniform", static_cast(Threads) * Iterations, 1); Run(*device, DivergentCode, "divergent", static_cast(Threads) / 2u * Iterations, 1); Run(*device, ParityCode, "split lanes", Threads / 2u, Repeats); 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/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 65fbaf2e73..ce8741e6ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -104,6 +104,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); context.Function("vkGetDeviceQueue")(context.device, family, 0u, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { release(); throw; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp index be56587bfc..8984302a91 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp @@ -84,6 +84,7 @@ public: context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { 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/PackedFloatColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp new file mode 100644 index 0000000000..9b322cb6d0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp @@ -0,0 +1,338 @@ +#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 +#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 Float11_11_10Info = 0x4171cu; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +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, 32> Programs{}; +std::size_t programCount = 0; + +std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { + const std::uint32_t infinity = 0x1fu << mantissaBits; + if (std::isnan(value)) { + const auto top = (std::bit_cast(value) & 0x7fffffu) >> (23u - mantissaBits); + return infinity | (top == 0u ? 1u : top); + } + if (!(value > 0.0f)) return 0; + if (std::isinf(value)) return infinity; + const double magnitude = value; + const double scale = std::ldexp(1.0, static_cast(mantissaBits)); + if (magnitude < std::ldexp(1.0, -14)) return static_cast(std::floor(magnitude * std::ldexp(1.0, 14) * scale)); + int exponent = 0; + const double fraction = std::frexp(magnitude, &exponent); + if (exponent - 1 > 15) return infinity - 1u; + return (static_cast(exponent - 1 + 15) << mantissaBits) | static_cast(std::floor((fraction * 2.0 - 1.0) * scale)); +} + +std::uint32_t Packed(std::array rgb) { + return SmallFloat(rgb[2], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[0], 6) << 21u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgb) { + return Program({std::bit_cast(rgb[0]), std::bit_cast(rgb[1]), std::bit_cast(rgb[2]), std::bit_cast(1.0f)}, false); +} + +std::span Float16Program(std::array rgb) { + return Program({Half(rgb[0]) | (Half(rgb[1]) << 16u), Half(rgb[2]) | (Half(1.0f) << 16u), 0, 0}, true); +} + +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) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float11_11_10Info; + 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_R32_UINT && color.elementBytes == 4 && color.componentMapping == 0xc6u && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT 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 = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT; + 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)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(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); + auto unpacked = fragment; + unpacked.context.pixel->targetExportPacking.fill(ShaderRecompiler::ColorExportPacking::None); + const auto plain = materialize(unpacked); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId && plain->variantId == pixelResult->variantId, "the draw did not use the registered pixel shader"); + Require(plain->PipelineVariantId() != pixelResult->PipelineVariantId() && materialize(fragment)->PipelineVariantId() == pixelResult->PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization"); + 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 / 4u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, 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) / 4u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? word : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%08x, expected 0x%08x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 4u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(index * 0x9e3779b9u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::array rgb) { + char text[96]; + std::snprintf(text, sizeof(text), "(%.9g, %.9g, %.9g)", rgb[0], rgb[1], rgb[2]); + return text; +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 4); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + const float nan = std::numeric_limits::quiet_NaN(); + const float infinity = std::numeric_limits::infinity(); + const std::array, 10> cases{{ + {1.0f, 0.5f, 0.25f}, + {2.0f, 0.75f, 65024.0f}, + {1.0f + 1.9f / 64.0f, 1.0f + 0.9f / 64.0f, 1.0f + 1.9f / 32.0f}, + {65000.0f, 70000.0f, 1.0e30f}, + {nan, -1.0f, infinity}, + {-infinity, -0.0f, std::numeric_limits::denorm_min()}, + {std::ldexp(1.0f, -15), std::ldexp(3.0f, -20), std::ldexp(1.0f, -19)}, + {std::ldexp(1.0f, -21), std::ldexp(1.9f, -20), std::ldexp(1.9f, -19)}, + {std::numeric_limits::min(), 0.1f, 1.0f / 3.0f}, + {3.0f, 5.0f, 7.0f}, + }}; + for (const auto& rgb : cases) { + Draw(device, color, Float32Export, Float32Program(rgb)); + ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); + before = ReadBack(device, block); + } + const std::array halves{0.5f, 0.75f, 0.125f}; + Draw(device, color, Float16Export, Float16Program(halves)); + ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("11_11_10 float 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/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 005a1e506f..5d747bf85a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -110,6 +110,7 @@ public: context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { 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/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 3a9d3178ef..9978674554 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -58,7 +58,12 @@ std::array BufferDescriptor(const void* data, std::uint32_t st return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; } -ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t inputs) { +enum class Emulation { + Geometry, + Tessellation, +}; + +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; @@ -98,9 +103,14 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa auto pixelResult = ShaderRecompiler::Recompile(fragment); std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; ShaderRecompiler::RecompileResult geometry; - if (pixelResult.barycentricEmulation.active) { + ShaderRecompiler::BarycentricTessellationShaders patches; + if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } else if (pixelResult.barycentricEmulation.active) { + patches = ShaderRecompiler::BuildBarycentricTessellationShaders(vertexResult, pixelResult, target, device.MaxFragmentInputComponents()); + shaders.push_back({ShaderStage::TessellationControl, &patches.control, 0}); + shaders.push_back({ShaderStage::TessellationEvaluation, &patches.evaluation, 0}); } shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); @@ -114,7 +124,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa 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.cullMode = cullMode; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; state.blends = {state.blend}; @@ -232,31 +242,56 @@ void CheckInterpolated() { Require(checked > 200u, "v_interp_p1/p2: too few interior pixels were checked"); } +bool Drawn() { + return std::any_of(Pixels.begin(), Pixels.end(), [](float value) { return value != Unwritten; }); +} + +void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + for (const auto cullMode : {VK_CULL_MODE_BACK_BIT, VK_CULL_MODE_FRONT_BIT}) { + Draw(device, emulation, InterpolatedPixelCode, perspective, cullMode); + const bool host = Drawn(); + Draw(device, emulation, BarycentricPixelCode, perspective, cullMode); + Require(Drawn() == host, name + ": culling the " + (cullMode == VK_CULL_MODE_BACK_BIT ? "back" : "front") + " face does not match the draw without emulation"); + } +} + +void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { + const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + const auto linear = PixelInputBit(PixelInput::LinearCenter); + auto result = Draw(device, emulation, BarycentricPixelCode, perspective); + Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); + const auto smooth = CheckBarycentrics(name + " PERSP_CENTER I/J", true); + result = Draw(device, emulation, BarycentricPixelCode, linear); + Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); + const auto noPerspective = CheckBarycentrics(name + " LINEAR_CENTER I/J", false); + result = Draw(device, emulation, VertexValuePixelCode, perspective); + Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); + CheckVertexValues(); + result = Draw(device, emulation, InterpolatedPixelCode, perspective); + Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); + CheckInterpolated(); + CheckWinding(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!device->GeometryLimits()) { - std::puts("skipped, the device lacks geometryShader"); + const bool geometry = device->GeometryLimits().has_value(); + const bool tessellation = device->Target().tessellation.has_value(); + if (!geometry && !tessellation) { + std::puts("skipped, the device lacks geometryShader and tessellationShader"); return VulkanTestSkipped; } - const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); - const auto linear = PixelInputBit(PixelInput::LinearCenter); - auto result = Draw(*device, BarycentricPixelCode, perspective); - Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); - const auto smooth = CheckBarycentrics("PERSP_CENTER I/J", true); - result = Draw(*device, BarycentricPixelCode, linear); - Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); - const auto noPerspective = CheckBarycentrics("LINEAR_CENTER I/J", false); - result = Draw(*device, VertexValuePixelCode, perspective); - Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); - CheckVertexValues(); - result = Draw(*device, InterpolatedPixelCode, perspective); - Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a geometry stage"); - CheckInterpolated(); - std::printf("barycentric emulation tests passed: %u perspective and %u linear I/J pixels, raw vertex values and host interpolation\n", smooth, noPerspective); + if (geometry) RunEmulation(*device, Emulation::Geometry); + else std::puts("geometry stage skipped, the device lacks geometryShader"); + if (tessellation) RunEmulation(*device, Emulation::Tessellation); + else std::puts("tessellation stages skipped, the device lacks tessellationShader"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp new file mode 100644 index 0000000000..d9b7114c0b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp @@ -0,0 +1,211 @@ +#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 Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +alignas(256) std::array Pixels{}; +alignas(256) std::array Results{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +struct Reduction { + const char* name; + std::uint32_t identity; + std::uint32_t vector; + std::uint32_t scalar; + std::function combine; +}; + +std::uint32_t Key(std::uint32_t pixel) { + return (pixel * 0x9e3779b1u) >> 22u; +} + +std::vector PixelCode(const Reduction& reduction) { + const std::uint32_t dpp = reduction.vector | 0x001a1afau; + return { + 0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, 0x000000c0, 0x7e120506, 0xd569000b, 0x0001ff06, + 0x9e3779b1, 0x2c161696, 0xbe98047e, 0x879a187e, 0xbeea287e, 0xd501000d, reduction.identity == 0u ? 0x006a1680u : 0x006a16c1u, + dpp, 0xff01110d, dpp, 0xff01120d, dpp, 0xff01140d, dpp, 0xff01180d, 0xbefe046a, + 0xd760000a, 0x00011f0d, 0xd760000b, 0x00013f0d, reduction.scalar | 0x000c0b0au, + 0xd760000a, 0x00015f0d, reduction.scalar | 0x000c0a0cu, 0xd760000a, 0x00017f0d, reduction.scalar | 0x000c0a0cu, + 0x8f099009, 0x880c090c, 0x7e0a020c, 0xe0702000, 0x80010506, 0x7e0e02f2, 0xf800180f, 0x07070707, + 0xbf810000, + }; +} + +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::vector> Triangles() { + std::vector> vertices; + constexpr std::uint32_t columns = 6; + constexpr std::uint32_t rows = 4; + for (std::uint32_t cell = 0; cell < columns * rows; ++cell) { + const float width = 2.0f / columns; + const float height = 2.0f / rows; + const float x = -1.0f + width * static_cast(cell % columns); + const float y = -1.0f + height * static_cast(cell / columns); + const float skew = 0.07f * static_cast(cell % 5u); + vertices.push_back({x + (0.05f + skew) * width, y + 0.05f * height, 0.5f, 1.0f}); + vertices.push_back({x + 0.93f * width, y + (0.11f + skew) * height, 0.5f, 1.0f}); + vertices.push_back({x + (0.37f - skew * 0.5f) * width, y + 0.91f * height, 0.5f, 1.0f}); + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, const ShaderRecompiler::SpirvTarget& target) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + Results.fill(Sentinel); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Results.data(), 4u, static_cast(Results.size())); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::uint32_t Check(const Reduction& reduction, std::uint32_t subgroupSize) { + const auto what = std::string("pixel row reduction ") + reduction.name; + std::map> waves; + for (std::uint32_t pixel = 0; pixel < Results.size(); ++pixel) { + if (std::to_integer(Pixels[pixel * 4u]) != 255u) { + Require(Results[pixel] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored " + std::to_string(Results[pixel])); + continue; + } + Require(Results[pixel] != Sentinel, what + ": covered pixel " + std::to_string(pixel) + " stored nothing"); + waves[Results[pixel] >> 16u].push_back(pixel); + } + std::uint32_t partial = 0; + std::size_t covered = 0; + for (const auto& [first, pixels] : waves) { + Require(std::find(pixels.begin(), pixels.end(), first) != pixels.end(), what + ": the first lane of the wave of pixel " + std::to_string(pixels.front()) + ", pixel " + std::to_string(first) + ", is not a covered pixel of that wave"); + std::uint32_t expected = reduction.identity; + for (const auto pixel : pixels) expected = reduction.combine(expected, Key(pixel)); + for (const auto pixel : pixels) { + Require((Results[pixel] & 0xffffu) == expected, what + ": pixel " + std::to_string(pixel) + " of a wave of " + std::to_string(pixels.size()) + " covered pixels read " + std::to_string(Results[pixel] & 0xffffu) + ", the wave holds " + std::to_string(expected)); + } + if (pixels.size() < subgroupSize) ++partial; + covered += pixels.size(); + } + Require(covered > 1000, what + ": the triangles covered only " + std::to_string(covered) + " pixels"); + Require(partial != 0u, what + ": no wave had fewer covered pixels than the host subgroup"); + return partial; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (!TargetHasCapability(target, spv::CapabilityGroupNonUniformArithmetic)) { + std::puts("skipped, the device has no subgroup arithmetic"); + return VulkanTestSkipped; + } + if (target.subgroupSize > 64u) { + std::printf("skipped, the %u-lane subgroup is wider than the wave\n", target.subgroupSize); + return VulkanTestSkipped; + } + auto wide = target; + wide.subgroupSize = 64u; + const std::array layouts{wide}; + const std::array reductions{{ + {"UMin", 0xffffffffu, 0x26000000u, 0x83800000u, [](std::uint32_t left, std::uint32_t right) { return std::min(left, right); }}, + {"UMax", 0u, 0x28000000u, 0x84800000u, [](std::uint32_t left, std::uint32_t right) { return std::max(left, right); }}, + {"IAdd", 0u, 0x4a000000u, 0x80000000u, [](std::uint32_t left, std::uint32_t right) { return left + right; }}, + {"AND", 0xffffffffu, 0x36000000u, 0x87000000u, [](std::uint32_t left, std::uint32_t right) { return left & right; }}, + {"OR", 0u, 0x38000000u, 0x88000000u, [](std::uint32_t left, std::uint32_t right) { return left | right; }}, + }}; + for (const auto& layout : layouts) { + for (const auto& reduction : reductions) { + for (const bool windowed : {false, true}) { + auto code = PixelCode(reduction); + if (windowed) code.insert(std::find(code.begin(), code.end(), 0x7e0e02f2u), 0x7e008514u); + std::uint32_t partial = 0; + for (std::uint32_t pass = 0; pass < 4; ++pass) { + Draw(*device, code, layout); + partial += Check(reduction, target.subgroupSize); + } + std::printf("%s%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, windowed ? " in a v_movreld_b32 window" : "", layout.subgroupSize, target.subgroupSize, partial); + } + } + } + std::puts("pixel row reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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..13e19d7dcb 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,11 +202,19 @@ 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"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { @@ -137,6 +239,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 +257,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 +553,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/RegistrationTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationTrace.cpp new file mode 100644 index 0000000000..e2c2e72152 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationTrace.cpp @@ -0,0 +1,83 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +alignas(256) const std::array PixelCode{0xbf810000u}; + +struct PixelHeader { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1b3, 2}, {0x1b4, 2}, {0x1b6, 1}}}; + + PixelHeader() { + const auto address = reinterpret_cast(PixelCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(PixelHeader); + shader.shader_size = sizeof(PixelCode); + shader.code = PixelCode.data(); + shader.type = 1; + registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00a, 0}, {0x00b, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + } +}; + +const char* CheckRegistration(bool disabled) { + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + auto* trace = AgcDriver::PerformanceTrace::Thread(); + if (disabled) return trace != nullptr ? "an unconfigured trace bound a buffer to a registering thread" : nullptr; + if (trace == nullptr) return "a title thread that registered a shader has no trace buffer"; + const auto snapshot = trace->Collect(); + if (snapshot.name != "title" || snapshot.dropped) return "the registering thread's trace identity or capacity is incorrect"; + const auto registration = [](const auto& event) { return event.scope != nullptr && std::string_view(event.scope) == "Shader.Register" && event.stage == nullptr && event.length > std::chrono::steady_clock::duration::zero(); }; + if (std::none_of(snapshot.slices.begin(), snapshot.slices.end(), registration) || std::none_of(snapshot.totals.begin(), snapshot.totals.end(), registration)) return "the registration was missing from the title thread's trace slices or totals"; + return nullptr; +} + +} + +int main(int argc, char** argv) { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + const bool disabled = argc > 1 && std::string_view(argv[1]) == "disabled"; + if (AgcDriver::PerformanceTrace::Configured() == disabled) throw std::runtime_error("unexpected trace configuration"); + if (!disabled) AgcDriver::PerformanceTrace::Present(1, 1, nullptr); + std::string failure = "the registering thread did not run"; + std::thread([&] { + try { + const char* message = CheckRegistration(disabled); + failure = message != nullptr ? message : ""; + } catch (const std::exception& error) { + failure = error.what(); + } + }).join(); + if (!failure.empty()) throw std::runtime_error(failure); + AgcDriverShutdown_nid_postfix(); + std::cout << "registration tracing checked\n"; + 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/ResourceCacheGrowth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp new file mode 100644 index 0000000000..12242e569c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp @@ -0,0 +1,182 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::size_t BaseEntries = 32; +constexpr std::size_t BaseShrinkWindow = 2; +constexpr std::uint32_t Dispatches = 48; +constexpr std::uint32_t Passes = 4; +constexpr std::uint32_t HotDispatches = 2; +constexpr std::uint32_t HotRepeats = 160; +constexpr std::size_t SrtBytes = 64; +constexpr std::size_t OutputBytes = 256; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t OutputOffset = SrtOffset + static_cast(Dispatches) * SrtBytes; +constexpr std::size_t BlockBytes = (OutputOffset + static_cast(Dispatches) * OutputBytes + 65535) & ~static_cast(65535); + +constexpr std::array StoreCode{0xf4080100u, 0xfa000000u, 0xbf8c0000u, 0x7e020281u, 0xe0700000u, 0x80010100u, 0xbf810000u}; + +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, "resource cache growth: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +std::vector Pm4(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Prepare(const GuestBlock& block) { + std::memcpy(block.Bytes(0), StoreCode.data(), sizeof(StoreCode)); + for (std::uint32_t dispatch = 0; dispatch < Dispatches; ++dispatch) { + const auto output = block.At(OutputOffset + static_cast(dispatch) * OutputBytes); + const std::array descriptor{static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(OutputBytes), 0x31016facu}; + std::memcpy(block.Bytes(SrtOffset + static_cast(dispatch) * SrtBytes), descriptor.data(), sizeof(descriptor)); + } + std::memset(block.Bytes(OutputOffset), 0, static_cast(Dispatches) * OutputBytes); +} + +void RunPass(const GuestBlock& block, std::uint32_t count = Dispatches, std::uint32_t repeats = 1) { + const auto kernel = block.At(0); + std::vector words; + for (std::uint32_t use = 0; use < count * repeats; ++use) { + const auto dispatch = use % count; + const auto srt = block.At(SrtOffset + static_cast(dispatch) * SrtBytes); + for (const auto& packet : {Pm4(0x76, {0x20c, static_cast(kernel >> 8u)}), Pm4(0x76, {0x20d, static_cast(kernel >> 40u) & 0xffu}), Pm4(0x76, {0x207, 64}), Pm4(0x76, {0x208, 1}), Pm4(0x76, {0x209, 1}), Pm4(0x76, {0x213, 2u << 1u}), Pm4(0x76, {0x240, static_cast(srt)}), Pm4(0x76, {0x241, static_cast(srt >> 32u)}), Pm4(0x15, {1, 1, 1, 0x41})}) { + words.insert(words.end(), packet.begin(), packet.end()); + } + } + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "resource cache growth: the dispatch submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void WorkingSetLargerThanTheInitialBound(const GuestBlock& block) { + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + for (std::uint32_t pass = 0; pass < Passes; ++pass) { + RunPass(block); + if (pass != 0) continue; + for (std::uint32_t dispatch = 0; dispatch < Dispatches; ++dispatch) { + std::uint32_t stored = 0; + AgcDriver::GuestMemory::Read(block.At(OutputOffset + static_cast(dispatch) * OutputBytes), std::as_writable_bytes(std::span(&stored, 1)), 4); + Require(stored == 1u, "resource cache growth: dispatch " + std::to_string(dispatch) + " stored " + std::to_string(stored) + ", expected 1"); + } + } + const auto kept = cache.Size(); + Require(kept >= Dispatches, "resource cache growth: " + std::to_string(Dispatches) + " dispatches with distinct resources repeated " + std::to_string(Passes) + " times in turn, but the resource cache kept only " + std::to_string(kept) + " entries (initial bound " + std::to_string(BaseEntries) + "), so every pass rebuilt every set"); +} + +void BoundComesBackDown(const GuestBlock& block) { + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + const auto grown = cache.Capacity(); + Require(grown > BaseEntries, "resource cache growth: the bound stayed at " + std::to_string(grown) + " entries after the large working set"); + RunPass(block, HotDispatches, HotRepeats); + const auto shrunk = cache.Capacity(); + Require(shrunk < grown, "resource cache growth: " + std::to_string(HotDispatches) + " sets served " + std::to_string(HotDispatches * HotRepeats) + " dispatches, but the bound stayed at " + std::to_string(shrunk) + " entries"); + + const std::uint32_t records = OutputBytes / 2; + std::memcpy(block.Bytes(SrtOffset + 8), &records, sizeof(records)); + RunPass(block, 1); + Require(cache.Size() <= shrunk, "resource cache growth: the bound came down to " + std::to_string(shrunk) + " entries, but the next insert left " + std::to_string(cache.Size())); + + for (std::uint32_t pass = 0; pass < Passes; ++pass) RunPass(block); + const auto regrown = cache.Capacity(); + Require(regrown > shrunk, "resource cache growth: the bound did not grow again when the large working set came back"); + RunPass(block, HotDispatches, HotRepeats); + Require(cache.Capacity() == regrown, "resource cache growth: the bound came down again after the same hot pass, although the last shrink had been undone"); + + cache.Clear(); + Require(cache.Capacity() == BaseEntries, "resource cache growth: Clear() left the bound at " + std::to_string(cache.Capacity()) + " entries instead of " + std::to_string(BaseEntries)); + Prepare(block); + for (std::uint32_t pass = 0; pass < Passes; ++pass) RunPass(block); + Require(cache.Capacity() > BaseEntries, "resource cache growth: after Clear() the large working set did not grow the bound again"); +} + +void UseTheResourceCacheOnEveryDispatch() { +#ifdef _WIN32 + _putenv_s("APS5_NO_DISPATCH_RECIPE", "1"); +#else + setenv("APS5_NO_DISPATCH_RECIPE", "1", 1); +#endif +} + +} + +int main() { + UseTheResourceCacheOnEveryDispatch(); + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + cache.SetBase(BaseEntries, BaseShrinkWindow); + cache.Clear(); + GuestBlock block; + Prepare(block); + WorkingSetLargerThanTheInitialBound(block); + BoundComesBackDown(block); + AgcDriverShutdown_nid_postfix(); + std::puts("resource cache growth tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index f3d7e2bf44..7b0d2b550b 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 { @@ -45,6 +57,8 @@ constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Unorm8x4SwappedInfo = 0x8828u; +constexpr std::uint32_t Format8888Unorm = 56; constexpr std::uint32_t Type2DMsaa = 14; constexpr std::uint32_t TileRenderTarget = 0x1b; constexpr std::uint32_t Threads = 32; @@ -54,6 +68,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, @@ -62,6 +78,10 @@ alignas(256) constexpr std::array VertexCode{ alignas(256) constexpr std::array PixelCode{ 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, }; +constexpr std::array DrawnUnorm8{64.0f / 255.0f, 128.0f / 255.0f, 32.0f / 255.0f, 1.0f}; +alignas(256) constexpr std::array Unorm8PixelCode{ + 0x7e0802ff, std::bit_cast(DrawnUnorm8[0]), 0x7e0a02ff, std::bit_cast(DrawnUnorm8[1]), 0x7e0c02ff, std::bit_cast(DrawnUnorm8[2]), 0x7e0e02ff, std::bit_cast(DrawnUnorm8[3]), 0xf800180f, 0x07060504, 0xbf810000, +}; alignas(256) constexpr std::array LoadCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, @@ -70,6 +90,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} }}; @@ -120,6 +152,39 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } +AgcDriver::Graphics::ColorTarget DecodeSwappedTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm8x4SwappedInfo; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_B8G8R8A8_UNORM && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the swapped 8_8_8_8 target did not decode as a 16x16 BGRA8 surface of 4 samples"); + return color; +} + +void CheckSwappedSamples(const std::vector>& loaded) { + const std::array swapped{DrawnUnorm8[2], DrawnUnorm8[1], DrawnUnorm8[0], DrawnUnorm8[3]}; + const auto codes = [](const std::array& texel) { + std::string text; + for (const auto value : texel) text += (text.empty() ? "" : ", ") + std::to_string(value * 255.0f); + return "(" + text + ")"; + }; + std::size_t drawn = 0; + for (const auto& texel : loaded) { + if (texel == std::array{}) continue; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + if (std::abs(texel[channel] - swapped[channel]) <= 0.5f / 255.0f) continue; + Require(false, "a sample of a BGRA8 target loaded through an RGBA8 texture reads " + codes(texel) + " in UNORM8 codes instead of the drawn color with red and blue swapped " + codes(swapped)); + } + ++drawn; + } + Require(drawn != 0, "no sample of the BGRA8 target holds the drawn color"); +} + AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; @@ -142,7 +207,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, std::span pixelCode = PixelCode) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -159,8 +224,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta vertex.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); - const auto codeAddress = reinterpret_cast(PixelCode.data()); - const std::span pixelCode(PixelCode); + 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{ @@ -181,15 +245,15 @@ 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); } -std::array TargetDescriptor(const Block& block, std::uint32_t sampleShift) { +std::array TargetDescriptor(const Block& block, std::uint32_t sampleShift, std::uint32_t format = Format16161616Float) { const auto address = block.Address(); return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (sampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), 0u, @@ -199,13 +263,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,9 +281,66 @@ ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, c return ShaderRecompiler::Recompile(request); } -std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { +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 = CompileLoad(device, TargetDescriptor(block, SampleShift), 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::uint32_t format = Format16161616Float) { + std::vector userData; + const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift, format), userData); std::vector> loaded(Side * Side * Samples); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { Buffer.fill(0xdeadbeefu); @@ -293,6 +413,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 +494,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 +645,30 @@ 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 swapped(4u * BlockBytes); + Draw(*device, TargetState(DecodeSwappedTarget(swapped)), 0, 2u, Unorm8PixelCode); + CheckSwappedSamples(LoadSamples(*device, swapped, Format8888Unorm)); + std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); + 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 d8a6076a3a..bdd7de4bc6 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" @@ -129,6 +130,7 @@ public: if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { @@ -236,6 +238,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 +321,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 +352,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 +378,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 +386,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) { @@ -622,6 +667,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(); @@ -639,6 +699,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) { @@ -671,6 +745,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/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp index add6ad6737..a7a25f9f88 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -96,6 +96,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 6c271a9f48..949c7ef5f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -1,10 +1,14 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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/UnitShadow.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" #ifndef _WIN32 #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include #include #include @@ -17,6 +21,7 @@ #include #include #include +#include #ifdef _WIN32 int main() { @@ -24,11 +29,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 +70,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 +86,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 +131,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; { @@ -137,8 +170,74 @@ int main() { Require(value == 0x5EEDF00Du, "sparse import: the GPU fill did not reach the backing memory across the chunk boundary"); } Require(mapping[boundary - 32769] == 0x11 && mapping[boundary + 32768] == 0x11, "sparse import: the GPU fill reached past its range"); - GuestAllocations::Mutation().Remove(mapping); + + constexpr std::uint64_t Lead = 16384; + auto* unaligned = mapping + Lead; + const auto unalignedBytes = bytes - 65536; + std::memset(mapping + boundary - 65536, 0x22, 131072); + const auto unalignedAddress = reinterpret_cast(unaligned); + device->ImportGuestMemory(Register(unaligned, unalignedBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + Require(Imported(device->Device(), unalignedAddress, static_cast(unalignedBytes)), "sparse import: a range whose backing offset is not 64 KiB aligned was not imported"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{0xA11CE5EDu, 0xA11CE5EDu, 0xA11CE5EDu, 0xA11CE5EDu}; + filled = device->FillBuffer(address + boundary - 32768, 65536, pattern); + device->WaitIdle(); + } + Require(filled, "sparse import: the fill across the chunk boundary of the unaligned range was not served by the import"); + for (std::uint64_t offset = boundary - 32768; offset < boundary + 32768; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, mapping + offset, sizeof(value)); + Require(value == 0xA11CE5EDu, "sparse import: the GPU fill of the unaligned range missed its guest address"); + } + Require(mapping[boundary - 32769] == 0x22 && mapping[boundary + 32768] == 0x22, "sparse import: the GPU fill of the unaligned range reached past its range"); + + if (AgcDriver::Graphics::UnitShadowEnabled()) { + using namespace AgcDriver::Graphics; + constexpr std::uint64_t Unit = 65536; + const auto unitBegin = ((unalignedAddress + Unit - 1) & ~(Unit - 1)) + Unit; + auto* const unitBytes = reinterpret_cast(unitBegin); + std::memset(unitBytes - Lead, 0x33, Unit + 2 * Lead); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(mapping, static_cast(mappingBytes)); + std::size_t published = 0; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + const auto* import = HostImportFor(context, unalignedAddress, static_cast(unalignedBytes)); + Require(import != nullptr, "sparse import: the unaligned range has no import"); + if (!import->dmaBuf) std::printf("note: the device imported the unaligned range through a host pointer, which has no lead\n"); + Require(import->lead == (import->dmaBuf ? Lead : 0), "sparse import: the unaligned import does not start its buffer at the block below it"); + auto* recorder = Recorder::Active(); + if (recorder != nullptr && AgcDriver::GuestMemory::Watched(unitBegin, Unit)) { + AgcDriver::GuestMemory::CollectWritesUncached(unitBegin, Unit); + const auto destination = ShadowDestinationFor(context, *import, unitBegin, unitBegin + Unit); + Require(destination.has_value(), "sparse import: no unit shadow for a whole unit of the unaligned import"); + const auto commands = recorder->Commands(); + recorder->Keep(destination->slab); + context.Function("vkCmdFillBuffer")(commands, destination->buffer, destination->offset, Unit, 0x5AD0BEEFu); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder->MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const ShadowedRange range{unitBegin, unitBegin + Unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), AgcDriver::GuestMemory::TrackerGeneration()); + published = PublishShadow(unitBegin, Unit, PublishScope::Whole, PublishReason::Hook); + recorder->Sync(); + } + } + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(mapping, static_cast(mappingBytes)); + if (published == 0) { + std::printf("note: the unaligned import's unit was not shadowed (no recorder or not watched); the publish was not checked\n"); + } else { + for (std::uint64_t offset = 0; offset < Unit; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, unitBytes + offset, sizeof(value)); + Require(value == 0x5AD0BEEFu, "sparse import: a unit shadow of the unaligned import was not published at its guest address"); + } + for (std::uint64_t offset = 0; offset < Lead; ++offset) Require((unitBytes - Lead)[offset] == 0x33 && unitBytes[Unit + offset] == 0x33, "sparse import: a unit shadow of the unaligned import was published outside its unit"); + } + } + GuestAllocations::Mutation().Remove(unaligned); + GuestAllocations::Mutation().Remove(probe); std::free(probe); std::cout << "AGC sparse dma-buf import tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp new file mode 100644 index 0000000000..389c56e3f4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp @@ -0,0 +1,323 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include +#endif + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +PFN_vkCmdBindPipeline NativeBind = nullptr; +std::atomic GraphicsBinds{0u}; + +VKAPI_ATTR void VKAPI_CALL CountBind(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) ++GraphicsBinds; + NativeBind(commands, point, pipeline); +} + +struct BindTrace { + AgcDriver::Graphics::DeviceFunctions* functions; + explicit BindTrace(const AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + functions = const_cast(context.functions); + Require(functions != nullptr && functions->cmdBindPipeline != nullptr, "native pipeline bind table is unavailable"); + NativeBind = functions->cmdBindPipeline; + functions->cmdBindPipeline = CountBind; + } + ~BindTrace() { functions->cmdBindPipeline = NativeBind; } +}; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array NullBindingTexture{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PlainPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array ColumnPixelCode{ + 0x7e040f00, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array RowPixelCode{ + 0x7e040f01, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array, 6> Vertices{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}, + {-1.0f, -1.0f, 0.75f, 1.0f}, {3.0f, -1.0f, 0.75f, 1.0f}, {-1.0f, 3.0f, 0.75f, 1.0f}, +}}; + +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}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { + const AgcDriver::Registers context{ + {0x1b3, 0x700u}, {0x1b4, 0x700u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, + }; + return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); +} + +constexpr std::uint32_t ImageElements = 2u; +constexpr std::uint32_t StorageFloat2DHeap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 2u; + +void Emit(std::vector& output, spv::Op opcode, std::initializer_list operands) { + output.push_back((static_cast(operands.size() + 1u) << spv::WordCountShift) | static_cast(opcode)); + output.insert(output.end(), operands.begin(), operands.end()); +} + +bool StartsDeclarations(spv::Op opcode) { + return opcode == spv::OpUndef || opcode == spv::OpVariable || (opcode >= spv::OpTypeVoid && opcode <= spv::OpTypeForwardPointer) || (opcode >= spv::OpConstantTrue && opcode <= spv::OpSpecConstantOp); +} + +void DeclareImageHeap(std::vector& module, std::uint32_t set, std::uint32_t binding, std::uint32_t elements) { + Require(module.size() >= 5u && module[0] == spv::MagicNumber, "the null-image fixture module is not SPIR-V"); + std::uint32_t scalar = 0u, floating = 0u; + std::size_t entry = 0u, declarations = 0u, functions = 0u; + for (std::size_t offset = 5u; offset < module.size();) { + const auto count = module[offset] >> spv::WordCountShift; + const auto opcode = static_cast(module[offset] & spv::OpCodeMask); + Require(count != 0u && count <= module.size() - offset, "the null-image fixture module has a truncated instruction"); + if (opcode == spv::OpFunction) { + functions = offset; + break; + } + if (opcode == spv::OpEntryPoint) { + Require(entry == 0u, "the null-image fixture module has several entry points"); + entry = offset; + } + if (declarations == 0u && StartsDeclarations(opcode)) declarations = offset; + if (opcode == spv::OpTypeInt && module[offset + 2u] == 32u && module[offset + 3u] == 0u) scalar = module[offset + 1u]; + if (opcode == spv::OpTypeFloat && module[offset + 2u] == 32u) floating = module[offset + 1u]; + offset += count; + } + Require(entry != 0u && declarations != 0u && functions != 0u && floating != 0u, "the null-image fixture module has no entry point, declarations or scalar shader types"); + auto id = module[3]; + const bool declareScalar = scalar == 0u; + if (declareScalar) scalar = id++; + const auto length = id++, image = id++, array = id++, pointer = id++, variable = id++; + std::vector annotations; + Emit(annotations, spv::OpDecorate, {variable, spv::DecorationDescriptorSet, set}); + Emit(annotations, spv::OpDecorate, {variable, spv::DecorationBinding, binding}); + std::vector types; + if (declareScalar) Emit(types, spv::OpTypeInt, {scalar, 32u, 0u}); + Emit(types, spv::OpConstant, {scalar, length, elements}); + Emit(types, spv::OpTypeImage, {image, floating, spv::Dim2D, 0u, 0u, 0u, 2u, spv::ImageFormatUnknown}); + Emit(types, spv::OpTypeArray, {array, image, length}); + Emit(types, spv::OpTypePointer, {pointer, spv::StorageClassUniformConstant, array}); + Emit(types, spv::OpVariable, {pointer, variable, spv::StorageClassUniformConstant}); + module.insert(module.begin() + static_cast(functions), types.begin(), types.end()); + module.insert(module.begin() + static_cast(declarations), annotations.begin(), annotations.end()); + if (module[1] >= 0x00010400u) { + module.insert(module.begin() + static_cast(entry + (module[entry] >> spv::WordCountShift)), variable); + module[entry] += 1u << spv::WordCountShift; + } + module[3] = id; +} + +void AttachNullImages(ShaderRecompiler::RecompileResult& result, const ShaderRecompiler::SpirvTarget& target, bool depthAlias = false) { + namespace Runtime = ShaderRecompiler::RuntimeAbi; + const auto address = static_cast(reinterpret_cast(depthAlias ? DepthMemory.data() : NullBindingTexture.data())); + const auto format = depthAlias ? 22u : 56u; + const std::array words{static_cast(address >> 8u), static_cast(address >> 40u) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + const auto set = target.registrationModules ? Runtime::FragmentDescriptorSet : Runtime::DescriptorSet; + const auto binding = Runtime::BindingNumber(Runtime::Stage::Fragment, static_cast(StorageFloat2DHeap)); + ShaderRecompiler::DescriptorBinding images{ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, set, binding, ImageElements, std::vector(ImageElements * words.size())}; + std::copy(words.begin(), words.end(), images.guestDescriptor.begin()); + images.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + images.imageWritten = {false, false}; + images.imageAtomic = {false, false}; + images.imageAtomic64 = {false, false}; + result.bindings.push_back(std::move(images)); + auto module = result.spirv.Copy(); + DeclareImageHeap(module, set, binding, ImageElements); + result.spirv = std::move(module); +} + +#if ANYPS5_ENABLE_SPIRV_TOOLS +void RequireValidModule(const ShaderRecompiler::RecompileResult& result, const ShaderRecompiler::SpirvTarget& target) { + std::string diagnostics; + spvtools::SpirvTools validator(target.spirvVersion >= 0x00010400u ? SPV_ENV_VULKAN_1_1_SPIRV_1_4 : SPV_ENV_VULKAN_1_1); + validator.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { + if (message != nullptr) diagnostics += std::string(message) + '\n'; + }); + const auto& words = result.spirv.Words(); + const bool valid = validator.Validate(words.data(), words.size()); + Require(valid, "the null-image fixture emitted invalid SPIR-V:\n" + diagnostics); +} +#endif + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, bool nullImages = false, bool depthAlias = false) { + Pixels.fill(Kept); + auto target = device.Target(); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast(spv::CapabilityStencilExportEXT)); + target.supportedCapabilities = capabilities; + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertexData.data(), 16u, static_cast(vertexData.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(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()); + + auto pixelUserData = pixelData.empty() ? std::vector(8, 0u) : std::vector(pixelData.begin(), pixelData.end()); + std::vector pixelMemory{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}; + pixelMemory.insert(pixelMemory.end(), pixelSrt.begin(), pixelSrt.end()); + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + auto pixelResult = ShaderRecompiler::Recompile(fragment); + if (nullImages) { + AttachNullImages(pixelResult, target, depthAlias); +#if ANYPS5_ENABLE_SPIRV_TOOLS + RequireValidModule(pixelResult, target); +#endif + } + if (!pixelResult.stencilReferences.empty()) { + Require(pixelResult.stencilReferenceDword * sizeof(std::uint32_t) + sizeof(std::uint32_t) == pixelResult.pushConstants.size(), "dynamic reference is not reserved after the stage payload"); + for (std::size_t cursor = 5; cursor < pixelResult.spirv.size();) { + const auto count = pixelResult.spirv[cursor] >> 16u; + Require(count != 0u && count <= pixelResult.spirv.size() - cursor, "invalid materialized replay module"); + Require((pixelResult.spirv[cursor] & 0xffffu) != spv::OpSpecConstant, "expected reference remains a pipeline specialization"); + cursor += count; + } + } + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + state.depthTest = depthFail; + state.depthCompare = depthFail ? VK_COMPARE_OP_NEVER : VK_COMPARE_OP_ALWAYS; + state.stencilTest = true; + state.stencilFront = stencil; + state.stencilBack = 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.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + if (alter) alter(state); + AgcDriver::Pm4::DrawParameters draw{0, vertices, 0, instances, 0, false}; + if (alterDraw) alterDraw(draw); + const auto bindsBefore = GraphicsBinds.load(); + device.Draw(state, draw, shaders); + device.WaitIdle(); + if (!pixelResult.stencilReferences.empty()) Require(GraphicsBinds.load() - bindsBefore == 1u, "stencil replay rebound its shader pipeline within an original draw"); +} + +void Check(const std::function& drawn, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = drawn(x, y) ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "stencil-rejected")); + } + } +} + +} + + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const BindTrace binds(*device); + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + constexpr std::array alternating{ + 0x7e040f00u, 0x36060481u, + 0x7c000000u | (0xc2u << 17u) | (3u << 9u) | 0x80u, + 0x7e0802ffu, 0xffu, + (1u << 25u) | (5u << 17u) | (4u << 9u) | 0x80u, + 0x7e0e02f2u, 0xf8000082u, 5u << 8u, + 0xf800180fu, 0x07070707u, 0xbf810000u + }; + const auto replace = [](std::uint32_t reference = 0u, std::uint32_t mask = 0xffu) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, mask, reference}; }; + const auto equal = [](std::uint32_t reference, std::uint32_t mask = 0xffu) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, mask, 0u, reference}; }; + const auto all = [](std::uint32_t, std::uint32_t) { return true; }; + const auto even = [](std::uint32_t x, std::uint32_t) { return x % 2u == 0u; }; + const auto odd = [](std::uint32_t x, std::uint32_t) { return x % 2u != 0u; }; + Draw(*device, PlainPixelCode, plain, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, true); + Check(all, "an empty typed image element remains supported without stencil export"); + Draw(*device, alternating, exported, replace(), 6u, 2u, false, {}, {}, Vertices, VertexCode, {}, {}, true); + Check(all, "an empty typed image element preserves stencil replay colors"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "an empty typed image element preserves the even stencil reference"); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(odd, "an empty typed image element preserves the odd stencil reference"); + bool refused = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, true, true); + } catch (const std::exception& error) { + const std::string_view reason(error.what()); + refused = reason.find("stencil replay refuses") != std::string_view::npos && reason.find("feedback") != std::string_view::npos; + if (!refused) throw; + } + Require(refused, "a non-null image aliasing the depth surface bypassed replay feedback checks"); + Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "refused depth feedback published color results"); + std::puts("stencil null feedback 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/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 63fbbb4539..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 @@ -220,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"); @@ -235,6 +237,34 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } +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. @@ -270,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"); @@ -384,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); @@ -444,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; @@ -453,6 +482,12 @@ 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"); 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/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/tests/test_guest_memory_lifetime.py b/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py new file mode 100644 index 0000000000..e5cc5d7467 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py @@ -0,0 +1,143 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +sys.path.insert(0, sys.argv[1]) +from test_guest_symbol_names import importing_executable, string_table, DYNAMIC, STRINGS, SYMBOLS, RELOCATIONS, GOT + +NAMES = ['Begin_nid_no_patch', 'mmap_nid_postfix', 'Fill_nid_no_patch', 'mprotect_nid_postfix', + 'Retired_nid_no_patch', 'Verify_nid_no_patch', 'sceKernelMapFlexibleMemory', + 'munmap_nid_postfix', 'Finish_nid_no_patch'] + + +def fixture(): + image = importing_executable(NAMES) + struct.pack_into(' #include #include +#include #include namespace { @@ -42,8 +44,10 @@ 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_robust2 = false; bool g_code = false; bool Replay(const char* path) { @@ -106,26 +110,43 @@ 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; } } try { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; + if (g_robust2) request.request.target.robustBufferAccess2 = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -147,7 +168,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 +184,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] [--robust2] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -184,6 +205,10 @@ int main(int argc, char** argv) { g_maintenance8 = true; continue; } + if (std::string(argv[i]) == "--robust2") { + g_robust2 = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; @@ -196,6 +221,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..fd0d7cf527 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -15,7 +15,6 @@ #include #include -static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; @@ -27,7 +26,8 @@ static std::uint32_t RemainingCommandBytes(const Apr::CommandBufferObject& buffe } static int Append(Apr::CommandBufferObject* buffer, const void* command, uint32_t bytes) { - if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; std::memcpy(buffer->base + buffer->offset, command, bytes); buffer->offset += bytes; ++buffer->numCommands; @@ -93,7 +93,8 @@ static std::uint64_t NopBytes(std::uint32_t dwords) { static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (!buffer) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = NopBytes(dwords); - if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; std::uint8_t* destination = buffer->base + buffer->offset; std::memset(destination, 0, bytes); @@ -116,7 +117,8 @@ static std::uint64_t MeasureMarker(const char* text) { static int AppendMarker(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, const char* text) { if (!buffer || !text) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = MarkerBytes(text); - if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; const Apr::MarkerCommand command{{opcode, static_cast(bytes)}}; std::uint8_t* destination = buffer->base + buffer->offset; std::memset(destination, 0, bytes); @@ -271,6 +273,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 +997,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..a9639ac279 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 @@ -94,6 +92,14 @@ static std::uint32_t bytesPerSample(Format f) { return formatIsFloat(f) ? sizeof(float) : sizeof(std::int16_t); } +static std::uint32_t checkedBufferSize(std::uint32_t frames, std::uint32_t channels, std::uint32_t bps) { + const std::uint64_t size = static_cast(frames) * channels * bps; + if (size > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("checkedBufferSize: audio buffer size overflows"); + } + return static_cast(size); +} + struct Port { bool used = false; int type = 0; @@ -110,7 +116,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; @@ -160,7 +166,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector const auto frames = port.samplesNum; const auto ch = static_cast(port.channels); const auto bps = bytesPerSample(port.format); - const auto size = frames * ch * bps; + const auto size = checkedBufferSize(frames, ch, bps); bool volumeChanged = port.mixLevel != DEFAULT_VOLUME; for (std::uint32_t i = 0; i < ch; i++) { @@ -227,8 +233,8 @@ static void queueAudio(Port& port, const void* data, std::unique_lock prepareBuf; const void* prepared = prepareBuffer(port, data, prepareBuf); - const std::uint32_t preparedSize = port.samplesNum * - static_cast(port.channels) * bytesPerSample(port.format); + const std::uint32_t preparedSize = checkedBufferSize(port.samplesNum, + static_cast(port.channels), bytesPerSample(port.format)); const void* queueData = prepared; const std::uint32_t queueSize = preparedSize; @@ -351,7 +357,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 +371,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 +391,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 +567,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/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/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index 84efff8feb..fc0c32cbf4 100644 --- a/core/libs/prx/libSceHmd2/CMakeLists.txt +++ b/core/libs/prx/libSceHmd2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceHmd2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHmd2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceHmd2 PRIVATE libc) configure_windows_unwind(libSceHmd2) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 9f1c4cb8f9..55d518e133 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,43 +1,92 @@ -#include -#include -#include "SceTypes.hpp" +#include "Hmd2.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include "SceTypes.hpp" + +#include +#include +#include namespace { -constexpr std::int32_t SCE_HMD2_ERROR_UNSUPPORTED_FEATURE = static_cast(0x81110016); +constexpr std::int32_t DeviceHandle = 0x0e010000; +constexpr Hmd2::FieldOfView DefaultFieldOfView{1.84177089f, 0.947190642f, 1.32898104f, 1.32898104f}; + +std::mutex stateMutex; +bool initialized = false; +std::int32_t openHandle = 0; + +std::uint32_t CompiledSdkVersion() { + static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, processParameters + 0x10, sizeof(version)); + return version; +} + +std::int32_t GetDeviceInformation(Hmd2::DeviceInformation* info) { + if (!info) return Hmd2::ParameterNull; + *info = {}; + if (!initialized) return Hmd2::NotInitialized; + info->status = Hmd2::DeviceStatus::NotDetected; + if (CompiledSdkVersion() < Hmd2::UserIdSdkVersion) info->userId = -1; + info->panelWidth = 4000; + info->panelHeight = 2040; + info->latency90Hz = 5555; + info->latency120Hz = 4160; + info->virtualImageDistance = 2.0f; + return 0; +} } extern "C" { +int APS5_VABI sceHmd2ReprojectionGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { NotImplemented_nid_no_patch(__func__); return 0; } std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { - (void)param; - return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; -} - -int APS5_VABI sceHmd2GetFieldOfView() { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(stateMutex); + if (initialized) return Hmd2::AlreadyInitialized; + if (!param) return Hmd2::ParameterNull; + initialized = true; return 0; } -int APS5_VABI sceHmd2GetDeviceInformationByHandle() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (handle == 0 || handle != openHandle) return Hmd2::InvalidHandle; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; return 0; } -int APS5_VABI sceHmd2Terminate() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + if (handle != openHandle) return Hmd2::InvalidHandle; + return GetDeviceInformation(info); +} + +std::int32_t APS5_VABI sceHmd2Terminate() { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + initialized = false; return 0; } -int APS5_VABI sceHmd2Close() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + if (handle != openHandle) return Hmd2::InvalidHandle; + openHandle = 0; return 0; } @@ -51,19 +100,30 @@ int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { return 0; } -int APS5_VABI sceHmd2GetDeviceInformation() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + return GetDeviceInformation(info); +} + +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; return 0; } -int APS5_VABI sceHmd2GetFieldOfViewWithoutHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param) { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + if (type != 0 || index != 0 || param) return Hmd2::ParameterInvalid; + if (userId != USER_SERVICE_INITIAL_USER_ID) { + if (CompiledSdkVersion() >= Hmd2::UserIdSdkVersion) return Hmd2::ParameterInvalid; + throw std::runtime_error("sceHmd2Open: user is not the initial user"); + } + if (openHandle != 0) return Hmd2::AlreadyOpened; + openHandle = DeviceHandle; + return DeviceHandle; } int APS5_VABI sceHmd2ReprojectionBeginFrame() { @@ -156,8 +216,10 @@ int APS5_VABI sceHmd2ReprojectionTerminate() { return 0; } -int APS5_VABI sceHmd2SetVibration() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level) { + (void)level; + std::lock_guard lock(stateMutex); + if (handle != openHandle) return Hmd2::InvalidHandle; return 0; } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp new file mode 100644 index 0000000000..3594b32bd1 --- /dev/null +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -0,0 +1,66 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHMD2_HMD2_HPP +#define CORE_LIBS_PRX_LIBSCEHMD2_HMD2_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include + +namespace Hmd2 { + +inline constexpr std::int32_t AlreadyInitialized = static_cast(0x8a720001); +inline constexpr std::int32_t NotInitialized = static_cast(0x8a720002); +inline constexpr std::int32_t InvalidHandle = static_cast(0x8a720003); +inline constexpr std::int32_t AlreadyOpened = static_cast(0x8a720006); +inline constexpr std::int32_t ParameterNull = static_cast(0x8a720008); +inline constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); + +inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; + +enum class DeviceStatus : std::uint32_t { + NotDetected = 2, +}; + +struct DeviceInformation { + DeviceStatus status; + std::int32_t userId; + std::uint32_t panelWidth; + std::uint32_t panelHeight; + std::uint16_t latency90Hz; + std::uint16_t latency120Hz; + std::uint8_t hmuMount; + std::uint8_t lensSeparationDistance; + std::uint8_t reserved[2]; + float virtualImageDistance; +}; + +struct FieldOfView { + float tanOut; + float tanIn; + float tanTop; + float tanBottom; +}; + +static_assert(sizeof(DeviceInformation) == 28); +static_assert(offsetof(DeviceInformation, userId) == 4); +static_assert(offsetof(DeviceInformation, panelWidth) == 8); +static_assert(offsetof(DeviceInformation, latency90Hz) == 16); +static_assert(offsetof(DeviceInformation, hmuMount) == 20); +static_assert(offsetof(DeviceInformation, virtualImageDistance) == 24); +static_assert(sizeof(FieldOfView) == 16); + +} + +extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param); +std::int32_t APS5_VABI sceHmd2Terminate(); +std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param); +std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle); +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info); +std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info); +std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView); +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView); +std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level); +} + +#endif diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 710a665726..f0c935f845 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -205,7 +205,8 @@ int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const std::string escaped; for (const char* c = in; *c; ++c) { const auto value = 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/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index c029f72ce6..8539bbd0c2 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceJobManager SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceJobManager PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver) configure_windows_unwind(libSceJobManager) if(BUILD_TESTING) add_test_executable(guest_job_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp) target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libc) + target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc) configure_windows_unwind(guest_job_manager_tests) foreach(testCase IN ITEMS memory validation dispatch ordering nested limits priority shutdown) add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase}) diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 94d53929a9..4c03e4254e 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -22,6 +22,24 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSiz int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** result); +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); +int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue eq, int id); +int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue eq, int id, void* udata); +int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev); +std::uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev); +int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata); +int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id); +int APS5_VABI sceAgcDriverSubmitAcb(std::uint32_t queue, const Packet* packet); +std::uint32_t* APS5_VABI sceAgcAcbAcquireMem(CommandBuffer* buf, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles); +std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs); +std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues); +std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); +std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); +std::uint32_t* APS5_VABI sceAgcCbDispatch(CommandBuffer* buf, std::uint32_t threadGroupX, std::uint32_t threadGroupY, std::uint32_t threadGroupZ, std::uint32_t modifier); +std::uint32_t* APS5_VABI sceAgcAcbDispatchIndirect(CommandBuffer* buf, const volatile void* indirectArguments, std::uint32_t modifier); +std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t action, std::uint16_t gcrControl, std::uint8_t dst, std::uint8_t cachePolicy, const volatile Label* address, std::uint8_t dataSelect, std::uint64_t data, std::uint16_t gdsOffset, std::uint16_t gdsSize, std::uint8_t interrupt, std::uint32_t interruptContextId); } namespace { @@ -50,6 +68,11 @@ constexpr std::uint64_t StateEnd = 0x2d8; constexpr std::uint64_t ThreadRecordBytes = 0x48; constexpr std::uint64_t SequenceBytes = 0x240; constexpr std::uint64_t NodeBytes = 72; +constexpr std::uint64_t ComputeContextBytes = 0x168; +constexpr std::uint64_t ComputeCommandBytes = 0x3ff * 104 + 0xff * 148 + 0x3f * 32; +constexpr std::uint32_t FirstComputeQueue = 0x20; +constexpr std::uint32_t UserDataRegister = 0x240; +constexpr int UserEventFilter = -11; struct MemorySizeQueryParams { std::uint32_t version; @@ -94,7 +117,7 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { struct Manager; class Sequence; -enum class ItemType { Dispatch, SyncPoint, Wait }; +enum class ItemType { Dispatch, SyncPoint, Wait, Compute, ComputeIndirect }; struct Item { ItemType type; @@ -105,6 +128,11 @@ struct Item { std::uint32_t handedOut; std::uint32_t completed; bool* passed; + const void* shader = nullptr; + std::array groups{}; + const void* indirectArguments = nullptr; + std::string name; + bool named = false; }; struct Counters { @@ -140,27 +168,27 @@ public: APS5_VABI int Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char* name) override; APS5_VABI int SyncPoint() override; APS5_VABI int Wait() override; - APS5_VABI int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) override { - NotImplemented_nid_no_patch("sce::Job compute dispatch"); - return 0; - } - APS5_VABI int DispatchComputeIndirect(const void*, void*, const void*, const char*) override { - NotImplemented_nid_no_patch("sce::Job indirect compute dispatch"); - return 0; - } + APS5_VABI int DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) override; + APS5_VABI int DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) override; bool Open(Manager* owner, const Counters& limit, std::uint32_t runQueue); int WaitAndClose(std::uint32_t index); void Advance(); void DispatchCompleted(std::uint32_t invocations); + void CollectCompute(); + bool Busy() const { return outstanding != 0; } Manager* manager = nullptr; bool open = false; + bool compute = false; + std::uint32_t queue = 0; private: int Latch(int code); void RequireOpen() const; int WaitLocked(std::unique_lock& guard); + int AppendCompute(Item item); + void SubmitCompute(const Item& item); Counters limits; Counters submitted; @@ -172,6 +200,11 @@ private: std::deque items; std::mutex waiters; std::condition_variable passed; + Label label{}; + std::uint64_t labelSeen = 0; + std::uint64_t labelTotal = 0; + bool batchStarted = false; + std::deque> commands; }; class Factory final : public ISequenceFactory { @@ -196,6 +229,11 @@ struct Manager { std::uint32_t persistentThreads = 0; std::array, 2> runQueues; std::vector sequences; + std::deque computeSequences; + std::uint32_t freeComputeSequences = 0; + std::vector computeQueues; + KernelEqueue computeEvents = 0; + Pthread computeHandler = nullptr; std::array factories; std::array unifiedFactories; std::vector workers; @@ -231,6 +269,8 @@ bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueu outstanding = 0; cursor = 0; items.clear(); + commands.clear(); + batchStarted = false; if (!TakeNode(*owner)) return false; open = true; return true; @@ -250,6 +290,12 @@ void Sequence::Advance() { manager->work.notify_all(); continue; } + if (item.type == ItemType::Compute || item.type == ItemType::ComputeIndirect) { + ++outstanding; + ++cursor; + SubmitCompute(item); + continue; + } if (outstanding != 0) return; if (item.type == ItemType::SyncPoint) { ++completed.syncPoints; @@ -260,6 +306,8 @@ void Sequence::Advance() { const auto retired = static_cast(cursor) + 1; items.erase(items.begin(), items.begin() + static_cast(retired)); cursor = 0; + commands.clear(); + batchStarted = false; if (error == 0) { submitted.invocations -= completed.invocations; submitted.dispatches -= completed.dispatches; @@ -296,6 +344,81 @@ int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, cons return 0; } +void Sequence::SubmitCompute(const Item& item) { + const auto* bytes = static_cast(item.shader); + const ShaderRegister* registers = nullptr; + const std::uint8_t* specials = nullptr; + std::memcpy(®isters, bytes + 0x20, sizeof(registers)); + std::memcpy(&specials, bytes + 0x28, sizeof(specials)); + const std::uint32_t registerCount = bytes[0x5c]; + std::uint32_t modifier = 0; + std::memcpy(&modifier, specials + 0x10, sizeof(modifier)); + const auto userData = reinterpret_cast(item.argument); + const std::array userDataWords{static_cast(userData), static_cast(userData >> 32)}; + auto& words = commands.emplace_back(64 + 3 * registerCount + item.name.size() / 4); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; + encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; + encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; + if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; + if ((modifier & 1) != 0) { + if (item.type == ItemType::Compute) { + encoded = encoded && sceAgcCbDispatch(&buffer, item.groups[0], item.groups[1], item.groups[2], modifier) != nullptr; + } else { + encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.indirectArguments, modifier) != nullptr; + } + } + if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x620, 0, 0, &label, 1, ++labelTotal, 0, 0, 2, 0) != nullptr; + if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); + batchStarted = true; + const Packet packet{words.data(), static_cast(buffer.cursor_up - words.data()), 0, {}}; + if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed"); +} + +void Sequence::CollectCompute() { + const std::uint64_t value = label.value; + const auto finished = static_cast(value - labelSeen); + labelSeen = value; + if (finished == 0) return; + completed.dispatches += finished; + outstanding -= finished; + if (outstanding == 0) Advance(); +} + +int Sequence::AppendCompute(Item item) { + const std::lock_guard guard(manager->lock); + RequireOpen(); + if (!compute) throw std::runtime_error("libSceJobManager: a compute dispatch was made on a CPU sequence"); + if (error != 0) return SequenceInvalid; + if (submitted.dispatches++ >= limits.dispatches) return Latch(SequenceLimitExceeded); + if (!TakeNode(*manager)) return Latch(SequenceNoNodes); + items.push_back(std::move(item)); + Advance(); + return 0; +} + +int Sequence::DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) { + if (shader == nullptr) return SequenceNullArgument; + if (x <= 0 || y <= 0 || z <= 0) return SequenceOutOfRange; + Item item{ItemType::Compute, this, nullptr, userData, 0, 0, 0, nullptr}; + item.shader = shader; + item.groups = {static_cast(x), static_cast(y), static_cast(z)}; + item.named = name != nullptr; + if (item.named) item.name = name; + return AppendCompute(std::move(item)); +} + +int Sequence::DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) { + if (shader == nullptr || indirectArguments == nullptr || (reinterpret_cast(indirectArguments) & 0x1f) != 0) return SequenceNullArgument; + Item item{ItemType::ComputeIndirect, this, nullptr, userData, 0, 0, 0, nullptr}; + item.shader = shader; + item.indirectArguments = indirectArguments; + item.named = name != nullptr; + if (item.named) item.name = name; + return AppendCompute(std::move(item)); +} + int Sequence::SyncPoint() { const std::lock_guard guard(manager->lock); RequireOpen(); @@ -318,10 +441,11 @@ int Sequence::WaitAndClose(std::uint32_t index) { const int result = WaitLocked(guard); ReturnNodes(*manager, static_cast(items.size()) + 1); items.clear(); + commands.clear(); cursor = 0; outstanding = 0; open = false; - manager->freeSequences |= std::uint32_t{1} << index; + (compute ? manager->freeComputeSequences : manager->freeSequences) |= std::uint32_t{1} << index; return result; } @@ -353,42 +477,63 @@ bool ValidLimits(std::uint32_t invocations, std::uint32_t dispatches, std::uint3 return invocations != 0 || dispatches != 0 || syncPoints != 0 || waits != 0; } -int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { +template +int CreateIn(Manager* manager, TSequences& sequences, std::uint32_t& free, std::uint32_t priority, ISequence** out, const Counters& limits) { if (out == nullptr) return SequenceNullArgument; - if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange; + if (!ValidLimits(limits.invocations, limits.dispatches, limits.syncPoints, limits.waits)) return SequenceOutOfRange; const std::lock_guard guard(manager->lock); - if (manager->freeSequences == 0) return SequenceNoneFree; - const int index = std::countr_zero(manager->freeSequences); - manager->freeSequences &= ~(std::uint32_t{1} << index); - Sequence& sequence = manager->sequences[static_cast(index)]; - if (!sequence.Open(manager, {invocations, dispatches, syncPoints, waits}, priority)) return SequenceNoNodes; + if (free == 0) return SequenceNoneFree; + const int index = std::countr_zero(free); + free &= ~(std::uint32_t{1} << index); + Sequence& sequence = sequences[static_cast(index)]; + if (!sequence.Open(manager, limits, priority)) return SequenceNoNodes; *out = &sequence; return 0; } -int Factory::Destroy(ISequence* sequence) { +template +int DestroyIn(Manager* manager, TSequences& sequences, const std::uint32_t& free, ISequence* sequence) { Sequence* found = nullptr; std::uint32_t index = 0; { const std::lock_guard guard(manager->lock); - for (; index < manager->sequences.size(); ++index) { - if (&manager->sequences[index] == sequence) break; + for (; index < sequences.size(); ++index) { + if (&sequences[index] == sequence) break; } - if (index == manager->sequences.size() || (manager->freeSequences & (std::uint32_t{1} << index)) != 0) return SequenceInvalid; - found = &manager->sequences[index]; + if (index == sequences.size() || (free & (std::uint32_t{1} << index)) != 0) return SequenceInvalid; + found = &sequences[index]; } static_cast(found->WaitAndClose(index)); return 0; } -int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { - if (out == nullptr) return SequenceNullArgument; - if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange; - return SequenceNoneFree; +int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { + return CreateIn(manager, manager->sequences, manager->freeSequences, priority, out, {invocations, dispatches, syncPoints, waits}); } -int Factory::DestroyCompute(ISequence*) { - return SequenceInvalid; +int Factory::Destroy(ISequence* sequence) { + return DestroyIn(manager, manager->sequences, manager->freeSequences, sequence); +} + +int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { + return CreateIn(manager, manager->computeSequences, manager->freeComputeSequences, priority, out, {invocations, dispatches, syncPoints, waits}); +} + +int Factory::DestroyCompute(ISequence* sequence) { + return DestroyIn(manager, manager->computeSequences, manager->freeComputeSequences, sequence); +} + +void* APS5_VABI ComputeHandlerEntry(void* argument) { + Manager& manager = *static_cast(argument); + for (;;) { + KernelEvent event{}; + int count = 0; + if (sceKernelWaitEqueue(manager.computeEvents, &event, 1, &count, nullptr) != 0) throw std::runtime_error("libSceJobManager: sceKernelWaitEqueue failed in the compute handler"); + if (count <= 0) continue; + if (sceKernelGetEventFilter(&event) == UserEventFilter && sceKernelGetEventId(&event) == 0) return nullptr; + const std::lock_guard guard(manager.lock); + for (Sequence& sequence : manager.computeSequences) sequence.CollectCompute(); + } } bool RunOne(Manager& manager, std::unique_lock& guard) { @@ -421,11 +566,16 @@ void* APS5_VABI WorkerEntry(void* argument) { return nullptr; } -std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, bool* fits) { +std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues, bool* fits) { const std::uint64_t end = memory + size; std::uint64_t address = AlignUp(memory + StateEnd, 8) + workers * ThreadRecordBytes; for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes; - address = AlignUp(AlignUp(address, 64) + sequences * SequenceBytes, 64); + address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; + if (computeQueues != 0) { + address = AlignUp(address + ComputeContextBytes, 0x100) + AlignUp(ComputeCommandBytes * computeQueues, 8); + address += AlignUp(4 * std::uint64_t{computeQueues}, 8) + 8 * std::uint64_t{computeQueues}; + } + address = AlignUp(address, 64); *fits = address <= end; return *fits ? static_cast(((end - address) / NodeBytes) & 0xffff) : 0; } @@ -462,6 +612,30 @@ void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t wo } } +void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) { + if (sceKernelCreateEqueue(&manager.computeEvents, "SceJobManagerComputeEventQueue") != 0) throw std::runtime_error("libSceJobManager: sceKernelCreateEqueue failed"); + for (std::uint64_t mask = computeQueueMask; mask != 0; mask &= mask - 1) { + const std::uint32_t queue = FirstComputeQueue + static_cast(std::countr_zero(mask)); + if (sceAgcDriverAddEqEvent(manager.computeEvents, static_cast(queue), nullptr) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverAddEqEvent failed"); + manager.computeQueues.push_back(queue); + } + manager.computeSequences.resize(computeSequences); + for (std::uint32_t index = 0; index < computeSequences; ++index) { + manager.computeSequences[index].compute = true; + manager.computeSequences[index].queue = manager.computeQueues[index]; + } + manager.freeComputeSequences = computeSequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << computeSequences) - 1; + const KernelSchedParam schedule{static_cast(priority)}; + PthreadAttr attributes = nullptr; + if (scePthreadAttrInit(&attributes) != 0 || scePthreadAttrSetinheritsched(&attributes, 0) != 0 || scePthreadAttrSetschedparam(&attributes, &schedule) != 0 || + scePthreadAttrSetstacksize(&attributes, 0x4000) != 0 || scePthreadAttrSetaffinity(&attributes, affinity) != 0) { + throw std::runtime_error("libSceJobManager: a compute handler thread attribute was rejected"); + } + const int created = scePthreadCreate(&manager.computeHandler, &attributes, ComputeHandlerEntry, &manager, "SceJobManagerComputeEventHdlr"); + scePthreadAttrDestroy(&attributes); + if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for the compute handler"); +} + int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t computeQueueMask, const ConfigParams* config, bool unified) { if (Implementation(self) != nullptr) return ManagerAlreadyInitialized; if (memory == nullptr) return ManagerNullMemory; @@ -469,7 +643,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke if ((base & 0xff) != 0) return ManagerMisalignedMemory; if (size == 0 || workers == 0 || workers > MaxWorkers) return ManagerInvalidArgument; if (unified) { - if ((sequences == 0 && computeSequences == 0) || sequences + computeSequences > MaxSequences || (computeQueueMask >> 56) != 0 || static_cast(std::popcount(computeQueueMask)) < computeSequences) return ManagerInvalidArgument; + if ((sequences == 0 && computeSequences == 0) || std::uint64_t{sequences} + computeSequences > MaxSequences || (computeQueueMask >> 56) != 0 || static_cast(std::popcount(computeQueueMask)) < computeSequences) return ManagerInvalidArgument; } else if (sequences == 0 || sequences > MaxSequences) { return ManagerInvalidArgument; } @@ -479,12 +653,16 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke const BlockHeader header{0x4d4a, static_cast(StateOffset), 0x05000001, static_cast(size), 0}; std::memcpy(memory, &header, sizeof(header)); if (config != nullptr && config->version > 1) return ManagerInvalidArgument; - if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::initialize with compute sequences"); const ConfigParams& settings = config != nullptr ? *config : DefaultConfig; + const auto computeQueues = computeSequences != 0 ? static_cast(std::popcount(computeQueueMask)) : 0; bool fits = false; - const std::uint32_t nodes = NodeCount(base, size, workers, sequences, &fits); + const std::uint32_t nodes = NodeCount(base, size, workers, sequences, computeQueues, &fits); if (!fits || nodes == 0) return ManagerMemoryTooSmall; if (const int invalid = ValidateWorkers(settings, workers); invalid != 0) return invalid; + const bool ownHandlerSettings = (settings.version & 1) != 0; + const std::uint32_t handlerPriority = ownHandlerSettings ? settings.computePriority : 0x100; + const std::uint32_t handlerAffinity = ownHandlerSettings ? settings.computeAffinity : 0x1fff; + if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument; auto* manager = new Manager; manager->running = true; manager->freeNodes = nodes - 1; @@ -496,6 +674,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke manager->unifiedFactories[priority].manager = manager; manager->unifiedFactories[priority].priority = priority; } + if (computeSequences != 0) StartCompute(*manager, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); Implementation(self) = manager; StartWorkers(*manager, settings, workers); return 0; @@ -539,12 +718,19 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN } const std::uint64_t total = params->sequences + computeSequences; if (params->invocations > total * MaxInvocations || params->dispatches > total * MaxDispatches || params->syncPoints > total * MaxSyncPoints || params->waits > total * MaxWaits) return 0; - if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::calculateRequiredMemorySize with compute sequences"); std::uint64_t bytes = (NodeBytes * params->workers + 0x317) & 0xfc0; bytes = (bytes + 0x13f) & ~std::uint64_t{0x3f}; bytes = (bytes + 0x0bf) & ~std::uint64_t{0x3f}; bytes = (bytes + 0x13f) & ~std::uint64_t{0x3f}; - bytes += params->sequences * SequenceBytes; + if (computeSequences != 0) { + const auto queues = static_cast(std::popcount(params->computeQueueMask)); + bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff}; + bytes = (bytes + ComputeCommandBytes * queues + 3) & ~std::uint64_t{3}; + bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7}; + bytes += 8 * queues; + } else { + bytes += params->sequences * SequenceBytes; + } const std::uint32_t nodes = params->dispatches + params->syncPoints + params->waits + static_cast(total) + 1; return AlignUp(bytes, 64) + std::uint64_t{nodes} * NodeBytes + 0x800; } @@ -563,12 +749,23 @@ int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { { const std::lock_guard guard(manager->lock); if (manager->blockedWaiters != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: a thread is still blocked in a sequence wait"); + for (const Sequence& sequence : manager->computeSequences) { + if (sequence.open && sequence.Busy()) throw std::runtime_error("sce::Job::JobManager::shutdown: a compute sequence still has work in flight"); + } manager->running = false; manager->work.notify_all(); } for (const Pthread worker : manager->workers) { if (scePthreadJoin(worker, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); } + if (manager->computeHandler != nullptr) { + if (sceKernelAddUserEventEdge(manager->computeEvents, 0) != 0 || sceKernelTriggerUserEvent(manager->computeEvents, 0, nullptr) != 0) { + throw std::runtime_error("sce::Job::JobManager::shutdown: the compute handler could not be signalled"); + } + if (scePthreadJoin(manager->computeHandler, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); + for (const std::uint32_t queue : manager->computeQueues) static_cast(sceAgcDriverDeleteEqEvent(manager->computeEvents, static_cast(queue))); + if (sceKernelDeleteEqueue(manager->computeEvents) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: sceKernelDeleteEqueue failed"); + } Implementation(self) = nullptr; delete manager; return 0; diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index b5dde2fe3f..1780e1d720 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -4,6 +4,7 @@ target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) configure_windows_unwind(libSceLibcInternal) target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp) +target_sources(libSceLibcInternal PRIVATE Inet.cpp) if(BUILD_TESTING) add_test_executable(guest_statvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestStatvfs.cpp) @@ -95,6 +96,16 @@ if(BUILD_TESTING) set_tests_properties(guest_getopt PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_inet_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestInetAddress.cpp) + target_include_directories(guest_inet_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_inet_address_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_inet_address_tests) + add_test(NAME guest_inet_address COMMAND guest_inet_address_tests) + set_tests_properties(guest_inet_address PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_inet_address PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) 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/libSceLibcInternal/Inet.cpp b/core/libs/prx/libSceLibcInternal/Inet.cpp new file mode 100644 index 0000000000..90996aacf6 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Inet.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity); +} + +namespace { + +constexpr int AddressFamilyInet = 2; +char g_ntoaBuffer[18]; + +bool IsDigit(unsigned char c) { return c >= '0' && c <= '9'; } +bool IsHexDigit(unsigned char c) { return IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } +bool IsSpace(unsigned char c) { return c == ' ' || (c >= '\t' && c <= '\r'); } + +} + +extern "C" { + +int APS5_VABI inet_aton_nid_postfix(const char* text, std::uint32_t* address) { + if (text == nullptr) throw std::invalid_argument("inet_aton: null text"); + std::uint64_t value = 0; + std::uint8_t parts[4] = {}; + std::uint8_t* part = parts; + bool digit = false; + auto c = static_cast(*text); + for (;;) { + if (!IsDigit(c)) return 0; + value = 0; + unsigned base = 10; + digit = false; + if (c == '0') { + c = static_cast(*++text); + if (c == 'x' || c == 'X') { + base = 16; + c = static_cast(*++text); + } else { + base = 8; + digit = true; + } + } + for (;;) { + if (IsDigit(c)) { + if (base == 8 && (c == '8' || c == '9')) return 0; + value = value * base + (c - '0'); + c = static_cast(*++text); + digit = true; + } else if (base == 16 && IsHexDigit(c)) { + value = (value << 4) | static_cast(c + 10 - (c >= 'a' ? 'a' : 'A')); + c = static_cast(*++text); + digit = true; + } else { + break; + } + } + if (c != '.') break; + if (part >= parts + 3 || value > 0xff) return 0; + *part++ = static_cast(value); + c = static_cast(*++text); + } + if (c != 0 && (c >= 0x80 || !IsSpace(c))) return 0; + if (!digit) return 0; + switch (part - parts + 1) { + case 2: + if (value > 0xffffff) return 0; + value |= static_cast(parts[0]) << 24; + break; + case 3: + if (value > 0xffff) return 0; + value |= (static_cast(parts[0]) << 24) | (static_cast(parts[1]) << 16); + break; + case 4: + if (value > 0xff) return 0; + value |= (static_cast(parts[0]) << 24) | (static_cast(parts[1]) << 16) | (static_cast(parts[2]) << 8); + break; + default: + break; + } + if (address != nullptr) { + const auto host = static_cast(value); + const std::uint8_t network[4] = {static_cast(host >> 24), static_cast(host >> 16), static_cast(host >> 8), static_cast(host)}; + std::memcpy(address, network, sizeof(network)); + } + return 1; +} + +std::uint32_t APS5_VABI inet_addr_nid_postfix(const char* text) { + std::uint32_t address = 0; + return inet_aton_nid_postfix(text, &address) ? address : 0xffffffffu; +} + +char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size) { + __inet_ntop_nid_postfix(AddressFamilyInet, &address, buffer, size); + return buffer; +} + +char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address) { + std::memcpy(g_ntoaBuffer, "[inet_ntoa error]", sizeof(g_ntoaBuffer)); + __inet_ntop_nid_postfix(AddressFamilyInet, &address, g_ntoaBuffer, sizeof(g_ntoaBuffer)); + return g_ntoaBuffer; +} + +} diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index c4571fb64e..c81a8fd0cb 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -60,6 +60,72 @@ int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes** a return 0; } +int APS5_VABI posix_spawnattr_getflags_nid_postfix(PosixSpawnAttributes* const* attributes, std::int16_t* flags) { + const auto& source = Attributes(attributes, "posix_spawnattr_getflags"); + if (flags == nullptr) throw std::invalid_argument("posix_spawnattr_getflags: null flags"); + *flags = source.flags; + return 0; +} + +int APS5_VABI posix_spawnattr_getpgroup_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* processGroup) { + const auto& source = Attributes(attributes, "posix_spawnattr_getpgroup"); + if (processGroup == nullptr) throw std::invalid_argument("posix_spawnattr_getpgroup: null process group"); + *processGroup = source.processGroup; + return 0; +} + +int APS5_VABI posix_spawnattr_getschedparam_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* schedulingParameter) { + const auto& source = Attributes(attributes, "posix_spawnattr_getschedparam"); + if (schedulingParameter == nullptr) throw std::invalid_argument("posix_spawnattr_getschedparam: null scheduling parameter"); + *schedulingParameter = source.schedulingPriority; + return 0; +} + +int APS5_VABI posix_spawnattr_getschedpolicy_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* schedulingPolicy) { + const auto& source = Attributes(attributes, "posix_spawnattr_getschedpolicy"); + if (schedulingPolicy == nullptr) throw std::invalid_argument("posix_spawnattr_getschedpolicy: null scheduling policy"); + *schedulingPolicy = source.schedulingPolicy; + return 0; +} + +int APS5_VABI posix_spawnattr_getsigdefault_nid_postfix(PosixSpawnAttributes* const* attributes, GuestSignalSet* signalDefault) { + const auto& source = Attributes(attributes, "posix_spawnattr_getsigdefault"); + if (signalDefault == nullptr) throw std::invalid_argument("posix_spawnattr_getsigdefault: null signal set"); + *signalDefault = source.signalDefault; + return 0; +} + +int APS5_VABI posix_spawnattr_getsigmask_nid_postfix(PosixSpawnAttributes* const* attributes, GuestSignalSet* signalMask) { + const auto& source = Attributes(attributes, "posix_spawnattr_getsigmask"); + if (signalMask == nullptr) throw std::invalid_argument("posix_spawnattr_getsigmask: null signal set"); + *signalMask = source.signalMask; + return 0; +} + +int APS5_VABI posix_spawnattr_setpgroup_nid_postfix(PosixSpawnAttributes** attributes, std::int32_t processGroup) { + Attributes(attributes, "posix_spawnattr_setpgroup").processGroup = processGroup; + return 0; +} + +int APS5_VABI posix_spawnattr_setschedparam_nid_postfix(PosixSpawnAttributes** attributes, const std::int32_t* schedulingParameter) { + auto& target = Attributes(attributes, "posix_spawnattr_setschedparam"); + if (schedulingParameter == nullptr) throw std::invalid_argument("posix_spawnattr_setschedparam: null scheduling parameter"); + target.schedulingPriority = *schedulingParameter; + return 0; +} + +int APS5_VABI posix_spawnattr_setschedpolicy_nid_postfix(PosixSpawnAttributes** attributes, std::int32_t schedulingPolicy) { + Attributes(attributes, "posix_spawnattr_setschedpolicy").schedulingPolicy = schedulingPolicy; + return 0; +} + +int APS5_VABI posix_spawnattr_setsigmask_nid_postfix(PosixSpawnAttributes** attributes, const GuestSignalSet* signalMask) { + auto& target = Attributes(attributes, "posix_spawnattr_setsigmask"); + if (signalMask == nullptr) throw std::invalid_argument("posix_spawnattr_setsigmask: null signal set"); + target.signalMask = *signalMask; + return 0; +} + int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions** actions) { if (actions == nullptr) throw std::invalid_argument("posix_spawn_file_actions_init: null file actions"); auto* created = new (std::nothrow) PosixSpawnFileActions{}; 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/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..e5537bf33d 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,19 @@ 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; } + const GuestArena::HostWrite lengthOutput(from == nullptr ? nullptr : fromlen, from == nullptr ? 0 : sizeof(*fromlen)); + if (!lengthOutput.Open()) return fail(NET_EFAULT); +#ifdef _WIN32 + if (const int wait = wait_for_local_address(s, socket, len == 0)) { + if (wait != RECEIVE_NOTHING) return fail(wait); + if (from) *fromlen = 0; + return 0; + } +#endif + const auto addressBytes = from != nullptr && socket.type == NET_SOCK_DGRAM ? + std::min(*fromlen, socket.family == NET_AF_INET ? 16u : 28u) : 0u; + const GuestArena::HostWrite addressOutput(addressBytes == 0 ? nullptr : from, addressBytes); + if (!addressOutput.Open()) return fail(NET_EFAULT); const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; @@ -969,7 +1053,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 +1090,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 +1142,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 +1161,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 +1185,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 +1197,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 +1210,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 +1724,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 f43d909d91..848133acd5 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -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/CMakeLists.txt b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt index 165feb0839..a7189fbc13 100644 --- a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt @@ -4,6 +4,9 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/GameInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GroupInfo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyStore.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyPackage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TitleTrophies.cpp ) target_include_directories(libSceNpTrophy2 PRIVATE @@ -23,3 +26,29 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(np_trophy2_store_tests EXCLUDE_FROM_ALL tests/TrophyStore.cpp) +target_include_directories(np_trophy2_store_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(np_trophy2_store_tests PRIVATE libSceNpTrophy2 libc) +configure_windows_unwind(np_trophy2_store_tests) +if(BUILD_TESTING) + set_target_properties(np_trophy2_store_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME np_trophy2_store COMMAND np_trophy2_store_tests) + if(WIN32) + set_tests_properties(np_trophy2_store PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(np_trophy2_package_tests EXCLUDE_FROM_ALL tests/TrophyPackage.cpp) +target_include_directories(np_trophy2_package_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(np_trophy2_package_tests PRIVATE libSceNpTrophy2 libc) +configure_windows_unwind(np_trophy2_package_tests) +if(BUILD_TESTING) + set_target_properties(np_trophy2_package_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME np_trophy2_package COMMAND np_trophy2_package_tests) + if(WIN32) + set_tests_properties(np_trophy2_package PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 8102f3785b..6a853b4c28 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -29,7 +29,8 @@ static constexpr std::uint32_t NP_TROPHY2_PROGRESS_PERCENTAGE = 0; static constexpr std::int32_t NP_TROPHY2_TROPHY_GRADE_BRONZE = 4; static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_NONE = 0; -static constexpr std::uint32_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0; +static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_VALUE = 1; +static constexpr std::uint64_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0; static constexpr std::size_t NP_TROPHY2_ICON_SIZE_NONE = 0; diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp index 30fadecda7..ecfde98357 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp @@ -4,7 +4,11 @@ #include #include -struct NpTrophy2Progress { std::uint32_t value; }; +struct NpTrophy2Progress { + std::int32_t type; + std::uint8_t reserved[4]; + std::uint64_t value; +}; struct NpTrophy2GameDetails { std::uint32_t num_groups; @@ -67,4 +71,12 @@ struct NpTrophy2Data { std::uint64_t timestamp_tick; }; +static_assert(sizeof(NpTrophy2Progress) == 16); +static_assert(sizeof(NpTrophy2GameDetails) == 152); +static_assert(sizeof(NpTrophy2GameData) == 24); +static_assert(sizeof(NpTrophy2GroupDetails) == 152); +static_assert(sizeof(NpTrophy2GroupData) == 32); +static_assert(sizeof(NpTrophy2Details) == 1312); +static_assert(sizeof(NpTrophy2Data) == 32); + #endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp new file mode 100644 index 0000000000..9d4653450c --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp @@ -0,0 +1,73 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +namespace NpTrophy2 { + +inline constexpr std::uint64_t MaxUcpSize = std::uint64_t{256} << 20u; + +struct TitleTrophies { + std::optional package; + std::optional rules; +}; + +std::string PackagePath(std::uint32_t serviceLabel); +std::string StatRulesPath(std::uint32_t serviceLabel); +TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language); +const TitleTrophies& Title(std::uint32_t serviceLabel); +const TrophyPackage* Package(std::uint32_t serviceLabel); +void CopyText(char* destination, std::size_t size, std::string_view text); + +using UnlockedQuery = std::function; + +UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel); + +struct StatUpdate { + std::uint64_t statId = 0; + std::uint64_t value = 0; + std::string input; +}; + +struct StatProblem { + std::string rule; + std::string reason; +}; + +struct StatChanges { + std::vector updates; + std::vector problems; +}; + +StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, StatStore& stats, std::int32_t userId, std::uint32_t serviceLabel); +std::vector ReachedTrophies(const TrophyPackage& package, const std::map& stats, const UnlockedQuery& unlocked); +std::optional PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked); +std::optional PostedUnlockRefusal(const TrophyPackage& package, std::int32_t trophyId); +std::optional ProgressTarget(const TrophyDefinition& trophy); +std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional stat, bool unlocked); + +struct TrophyCounts { + std::uint32_t trophies = 0; + std::uint32_t unlocked = 0; + std::array grades{}; + std::array unlockedGrades{}; + + std::uint32_t Percentage() const; +}; + +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional group, const UnlockedQuery& unlocked); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp new file mode 100644 index 0000000000..4be4238929 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -0,0 +1,117 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +inline constexpr std::uint32_t UcpMagic = 0xB228C60Au; +inline constexpr std::uint32_t UcpVersion = 1; +inline constexpr std::uint64_t UcpHeaderSize = 0x40; +inline constexpr std::uint64_t UcpTocHeaderSize = 0x20; +inline constexpr std::uint64_t UcpEntrySize = 0x40; +inline constexpr std::size_t UcpNameSize = 0x20; +inline constexpr std::uint64_t UcpMaxEntries = 4096; + +using UcpFiles = std::map>; + +UcpFiles ReadUcp(std::string_view data); + +class Json { +public: + enum class Kind { Null, Bool, Number, String, Array, Object }; + + Kind kind = Kind::Null; + bool boolean = false; + std::string text; + std::vector items; + std::vector> members; + + const Json* Find(std::string_view key) const; + bool IsTrue() const; + std::optional Unsigned() const; + std::optional Id() const; +}; + +Json ParseJson(std::string_view text); + +inline constexpr std::int32_t GradePlatinum = 1; +inline constexpr std::int32_t GradeGold = 2; +inline constexpr std::int32_t GradeSilver = 3; +inline constexpr std::int32_t GradeBronze = 4; +inline constexpr std::int32_t BaseGroup = -1; +inline constexpr std::size_t MaxTrophies = 1000; +inline constexpr std::size_t MaxGroups = 50; + +enum class Comparator { None, GreaterEqual, Greater, LessEqual, Less }; + +struct UnlockCondition { + std::uint64_t statId = 0; + std::uint64_t target = 0; + Comparator comparator = Comparator::None; + + bool Reached(std::uint64_t value) const; +}; + +struct TrophyDefinition { + std::int32_t id = 0; + std::int32_t grade = GradeBronze; + std::int32_t groupId = BaseGroup; + bool hidden = false; + bool hasReward = false; + bool progressive = false; + std::optional platinumId; + std::optional statId; + std::optional target; + Comparator comparator = Comparator::None; + std::string name; + std::string detail; + std::string reward; + + std::optional Condition() const; +}; + +struct TrophyPackage { + std::string language; + std::string title; + std::map trophies; + std::map groups; + std::vector skipped; +}; + +TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language); + +enum class StatType { UInt32, UInt64 }; + +struct StatDefinition { + StatType type = StatType::UInt64; + std::uint64_t minimum = 0; + std::uint64_t maximum = UINT64_MAX; +}; + +struct StatRule { + std::string input; + std::uint64_t statId = 0; +}; + +struct StatRules { + std::map stats; + std::map, std::less<>> events; + std::vector skipped; +}; + +StatRules ParseStatRules(std::string_view ucp); + +std::string_view LanguageTag(int consoleLanguage); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp new file mode 100644 index 0000000000..d9ab341ae0 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -0,0 +1,110 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYSTORE_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYSTORE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NpTrophy2Unlock { + std::string titleId; + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; + std::int32_t trophyId = 0; + std::uint64_t tick = 0; +}; + +using NpTrophy2UnlockListener = void (*)(void* context, const NpTrophy2Unlock& unlock); + +struct NpTrophy2EventProperty { + std::string path; + std::optional value; + std::string text; +}; + +struct NpTrophy2Event { + std::string name; + std::vector properties; +}; + +namespace NpTrophy2 { + +inline constexpr char TrophyRootGuest[] = "/_trophy"; +inline constexpr std::uint64_t UnixEpochTick = 62135596800000000ull; + +std::uint64_t CurrentTick(); + +class TrophyStore { +public: + explicit TrophyStore(std::filesystem::path directory); + + bool Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick); + std::optional UnlockTick(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); + std::size_t UnlockCount(std::int32_t userId, std::uint32_t serviceLabel); + std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; + +private: + using Key = std::pair; + using Unlocks = std::map; + + Unlocks& record(const Key& key); + void save(const Key& key, const Unlocks& unlocks) const; + + std::filesystem::path directory; + std::mutex mutex; + std::map records; +}; + +TrophyStore& TitleStore(); + +class StatStore { +public: + explicit StatStore(std::filesystem::path directory); + + void Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); + std::optional Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId); + std::map Values(std::int32_t userId, std::uint32_t serviceLabel); + std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; + +private: + using Key = std::pair; + using Stats = std::map; + + Stats& record(const Key& key); + + std::filesystem::path directory; + std::mutex mutex; + std::map records; +}; + +StatStore& TitleStats(); + +struct ContextOwner { + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; +}; + +int CreateContext(std::int32_t userId, std::uint32_t serviceLabel); +void DestroyContext(int context); +std::optional FindContext(int context); +std::optional ContextUnlockTick(int context, std::int32_t trophyId); +std::size_t ContextUnlockCount(int context); +bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); + +} + +extern "C" { + +bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); +std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context); +void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id); +void NpTrophy2ApplyEvent_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, const NpTrophy2Event& event); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index 74048df024..ef3a23e905 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -1,25 +1,68 @@ +#include #include +#include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" +namespace NpTrophy2 { + +namespace { + +struct Contexts { + std::mutex mutex; + std::map owners; + int next = NP_TROPHY2_CONTEXT_DEFAULT; +}; + +Contexts& contexts() { + static Contexts table; + return table; +} + +} + +int CreateContext(std::int32_t userId, std::uint32_t serviceLabel) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + const int context = table.next++; + table.owners[context] = {userId, serviceLabel}; + return context; +} + +void DestroyContext(int context) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + table.owners.erase(context); +} + +std::optional FindContext(int context) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + const auto found = table.owners.find(context); + if (found == table.owners.end()) return std::nullopt; + return found->second; +} + +} + extern "C" { int APS5_VABI sceNpTrophy2CreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) { if (context == nullptr) { APS5_INVALID_ARG_EX; } - (void)user_id; - (void)service_label; (void)options; - *context = NP_TROPHY2_CONTEXT_DEFAULT; + *context = NpTrophy2::CreateContext(user_id, service_label); return SCE_NP_TROPHY2_OK; } int APS5_VABI sceNpTrophy2DestroyContext(int context) { - (void)context; + NpTrophy2::DestroyContext(context); return SCE_NP_TROPHY2_OK; } @@ -35,7 +78,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/libSceNpTrophy2/src/GameInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp index 0c5b320f6f..dc11f536ef 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp @@ -1,9 +1,12 @@ #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" extern "C" { @@ -12,26 +15,50 @@ int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDeta if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + if (details != nullptr) { + std::memset(details, 0, sizeof(*details)); + details->num_groups = NP_TROPHY2_NUM_GROUPS; + details->num_trophies = NP_TROPHY2_NUM_TROPHIES; + details->num_platinum = NP_TROPHY2_NUM_PLATINUM; + details->num_gold = NP_TROPHY2_NUM_GOLD; + details->num_silver = NP_TROPHY2_NUM_SILVER; + details->num_bronze = NP_TROPHY2_NUM_BRONZE; + std::strncpy(details->title, NP_TROPHY2_GAME_TITLE, sizeof(details->title) - 1); + } + if (data != nullptr) { + std::memset(data, 0, sizeof(*data)); + data->unlocked_trophies = static_cast(NpTrophy2::ContextUnlockCount(context)); + data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; + data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; + data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; + data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; + data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + } + return SCE_NP_TROPHY2_OK; + } + const auto counts = NpTrophy2::CountTrophies(*package, std::nullopt, NpTrophy2::UnlockedBy(owner->userId, owner->serviceLabel)); if (details != nullptr) { std::memset(details, 0, sizeof(*details)); - details->num_groups = NP_TROPHY2_NUM_GROUPS; - details->num_trophies = NP_TROPHY2_NUM_TROPHIES; - details->num_platinum = NP_TROPHY2_NUM_PLATINUM; - details->num_gold = NP_TROPHY2_NUM_GOLD; - details->num_silver = NP_TROPHY2_NUM_SILVER; - details->num_bronze = NP_TROPHY2_NUM_BRONZE; - std::strncpy(details->title, NP_TROPHY2_GAME_TITLE, sizeof(details->title) - 1); + details->num_groups = static_cast(package->groups.size()); + details->num_trophies = counts.trophies; + details->num_platinum = counts.grades[NpTrophy2::GradePlatinum]; + details->num_gold = counts.grades[NpTrophy2::GradeGold]; + details->num_silver = counts.grades[NpTrophy2::GradeSilver]; + details->num_bronze = counts.grades[NpTrophy2::GradeBronze]; + NpTrophy2::CopyText(details->title, sizeof(details->title), package->title); } if (data != nullptr) { std::memset(data, 0, sizeof(*data)); - data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; - data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; - data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; - data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; - data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; - data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + data->unlocked_trophies = counts.unlocked; + data->unlocked_platinum = counts.unlockedGrades[NpTrophy2::GradePlatinum]; + data->unlocked_gold = counts.unlockedGrades[NpTrophy2::GradeGold]; + data->unlocked_silver = counts.unlockedGrades[NpTrophy2::GradeSilver]; + data->unlocked_bronze = counts.unlockedGrades[NpTrophy2::GradeBronze]; + data->progress_percentage = counts.Percentage(); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp index 5e97582aa7..887423ddc8 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp @@ -1,41 +1,87 @@ +#include #include #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void fillPlaceholderDetails(std::int32_t groupId, NpTrophy2GroupDetails* details) { + std::memset(details, 0, sizeof(*details)); + details->group_id = groupId; + details->num_trophies = NP_TROPHY2_NUM_TROPHIES; + details->num_platinum = NP_TROPHY2_NUM_PLATINUM; + details->num_gold = NP_TROPHY2_NUM_GOLD; + details->num_silver = NP_TROPHY2_NUM_SILVER; + details->num_bronze = NP_TROPHY2_NUM_BRONZE; + std::strncpy(details->title, NP_TROPHY2_GROUP_TITLE, sizeof(details->title) - 1); +} + +void fillPlaceholderData(std::int32_t groupId, NpTrophy2GroupData* data) { + std::memset(data, 0, sizeof(*data)); + data->group_id = groupId; + data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; + data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; + data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; + data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; + data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; + data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; +} + +void fillGroup(const NpTrophy2::TrophyPackage& package, const NpTrophy2::ContextOwner& owner, std::int32_t groupId, const std::string& title, NpTrophy2GroupDetails* details, + NpTrophy2GroupData* data) { + const auto counts = NpTrophy2::CountTrophies(package, groupId, NpTrophy2::UnlockedBy(owner.userId, owner.serviceLabel)); + if (details != nullptr) { + std::memset(details, 0, sizeof(*details)); + details->group_id = groupId; + details->num_trophies = counts.trophies; + details->num_platinum = counts.grades[NpTrophy2::GradePlatinum]; + details->num_gold = counts.grades[NpTrophy2::GradeGold]; + details->num_silver = counts.grades[NpTrophy2::GradeSilver]; + details->num_bronze = counts.grades[NpTrophy2::GradeBronze]; + NpTrophy2::CopyText(details->title, sizeof(details->title), title); + } + if (data != nullptr) { + std::memset(data, 0, sizeof(*data)); + data->group_id = groupId; + data->unlocked_trophies = counts.unlocked; + data->unlocked_platinum = counts.unlockedGrades[NpTrophy2::GradePlatinum]; + data->unlocked_gold = counts.unlockedGrades[NpTrophy2::GradeGold]; + data->unlocked_silver = counts.unlockedGrades[NpTrophy2::GradeSilver]; + data->unlocked_bronze = counts.unlockedGrades[NpTrophy2::GradeBronze]; + data->progress_percentage = counts.Percentage(); + } +} + +} + extern "C" { int APS5_VABI sceNpTrophy2GetGroupInfo(int context, int handle, int group_id, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) { if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const std::int32_t normalized_group_id = (group_id < 0 ? NP_TROPHY2_GROUP_ID_BASE : group_id); - if (details != nullptr) { - std::memset(details, 0, sizeof(*details)); - details->group_id = normalized_group_id; - details->num_trophies = NP_TROPHY2_NUM_TROPHIES; - details->num_platinum = NP_TROPHY2_NUM_PLATINUM; - details->num_gold = NP_TROPHY2_NUM_GOLD; - details->num_silver = NP_TROPHY2_NUM_SILVER; - details->num_bronze = NP_TROPHY2_NUM_BRONZE; - std::strncpy(details->title, NP_TROPHY2_GROUP_TITLE, sizeof(details->title) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const std::int32_t normalized_group_id = (group_id < 0 ? NP_TROPHY2_GROUP_ID_BASE : group_id); + if (details != nullptr) fillPlaceholderDetails(normalized_group_id, details); + if (data != nullptr) fillPlaceholderData(normalized_group_id, data); + return SCE_NP_TROPHY2_OK; } - if (data != nullptr) { - std::memset(data, 0, sizeof(*data)); - data->group_id = normalized_group_id; - data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; - data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; - data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; - data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; - data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; - data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + const auto group = package->groups.find(group_id); + if (group == package->groups.end()) { + throw std::invalid_argument(std::string(__func__) + ": group " + std::to_string(group_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } + fillGroup(*package, *owner, group->first, group->second, details, data); return SCE_NP_TROPHY2_OK; } @@ -43,29 +89,21 @@ int APS5_VABI sceNpTrophy2GetGroupInfoArray(int context, int handle, uint32_t of if (count == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); - *count = out_count; - if (out_count != 0 && details_array != nullptr) { - std::memset(details_array, 0, sizeof(*details_array)); - details_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - details_array->num_trophies = NP_TROPHY2_NUM_TROPHIES; - details_array->num_platinum = NP_TROPHY2_NUM_PLATINUM; - details_array->num_gold = NP_TROPHY2_NUM_GOLD; - details_array->num_silver = NP_TROPHY2_NUM_SILVER; - details_array->num_bronze = NP_TROPHY2_NUM_BRONZE; - std::strncpy(details_array->title, NP_TROPHY2_GROUP_TITLE, sizeof(details_array->title) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); + *count = out_count; + if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_GROUP_ID_BASE, details_array); + if (out_count != 0 && data_array != nullptr) fillPlaceholderData(NP_TROPHY2_GROUP_ID_BASE, data_array); + return SCE_NP_TROPHY2_OK; } - if (out_count != 0 && data_array != nullptr) { - std::memset(data_array, 0, sizeof(*data_array)); - data_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - data_array->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; - data_array->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; - data_array->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; - data_array->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; - data_array->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; - data_array->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + *count = 0; + auto group = package->groups.begin(); + std::advance(group, std::min(offset, package->groups.size())); + for (; group != package->groups.end() && *count < limit; ++group, ++*count) { + fillGroup(*package, *owner, group->first, group->second, details_array != nullptr ? &details_array[*count] : nullptr, data_array != nullptr ? &data_array[*count] : nullptr); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp new file mode 100644 index 0000000000..813fa576f7 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -0,0 +1,240 @@ +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +bool tracing() { + static const bool trace = std::getenv("APS5_TRACE_UDS") != nullptr; + return trace; +} + +std::string labelled(const char* format, std::uint32_t serviceLabel) { + char path[64]; + std::snprintf(path, sizeof(path), format, static_cast(serviceLabel)); + return path; +} + +std::optional readTitleFile(const std::string& guestPath) { + const auto path = ResolvePath_nid_no_patch(guestPath.c_str()); + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) throw std::filesystem::filesystem_error("cannot check the file", path, error); + if (!exists) return std::nullopt; + const auto size = std::filesystem::file_size(path); + if (size > MaxUcpSize) throw std::runtime_error("it has " + std::to_string(size) + " bytes, more than " + std::to_string(MaxUcpSize)); + std::string bytes(static_cast(size), '\0'); + std::ifstream file(path, std::ios::binary); + if (!file.read(bytes.data(), static_cast(bytes.size()))) throw std::runtime_error("cannot read " + path.string()); + return bytes; +} + +template +std::optional loadTitleFile(const std::string& guestPath, const char* without, TParse parse) { + std::optional bytes; + try { + bytes = readTitleFile(guestPath); + } catch (const std::runtime_error& error) { + std::fprintf(stderr, "[trophy] %s is not used, so %s: %s\n", guestPath.c_str(), without, error.what()); + return std::nullopt; + } + if (!bytes) { + if (tracing()) std::fprintf(stderr, "[uds] %s does not exist, so %s\n", guestPath.c_str(), without); + return std::nullopt; + } + try { + return parse(*bytes); + } catch (const std::runtime_error& error) { + std::fprintf(stderr, "[trophy] %s is malformed, so %s: %s\n", guestPath.c_str(), without, error.what()); + return std::nullopt; + } +} + +std::string counted(std::size_t count, const char* one, const char* many) { + return std::to_string(count) + " " + (count == 1 ? one : many); +} + +bool firstProblem(const std::string& rule) { + static std::mutex mutex; + static std::set reported; + std::lock_guard lock(mutex); + return reported.insert(rule).second; +} + +} + +std::string PackagePath(std::uint32_t serviceLabel) { + return labelled("/app0/sce_sys/trophy2/trophy%02u.ucp", serviceLabel); +} + +std::string StatRulesPath(std::uint32_t serviceLabel) { + return labelled("/app0/sce_sys/uds/uds%02u.ucp", serviceLabel); +} + +TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language) { + TitleTrophies title; + const auto packagePath = PackagePath(serviceLabel); + title.package = loadTitleFile(packagePath, "trophy queries report placeholders and stats unlock no trophies", + [language](std::string_view bytes) { return ParseTrophyPackage(bytes, language); }); + if (title.package) { + std::fprintf(stderr, "[trophy] %s: %s, %s, texts in %s\n", packagePath.c_str(), counted(title.package->trophies.size(), "trophy", "trophies").c_str(), + counted(title.package->groups.size(), "group", "groups").c_str(), title.package->language.c_str()); + for (const auto& skipped : title.package->skipped) std::fprintf(stderr, "[trophy] %s: %s\n", packagePath.c_str(), skipped.c_str()); + } + const auto rulesPath = StatRulesPath(serviceLabel); + title.rules = loadTitleFile(rulesPath, "posted events update no stats", [](std::string_view bytes) { return ParseStatRules(bytes); }); + if (title.rules) { + std::size_t count = 0; + for (const auto& [name, rules] : title.rules->events) count += rules.size(); + std::fprintf(stderr, "[trophy] %s: %s, %s for %s\n", rulesPath.c_str(), counted(title.rules->stats.size(), "stat", "stats").c_str(), counted(count, "rule", "rules").c_str(), + counted(title.rules->events.size(), "event", "events").c_str()); + for (const auto& skipped : title.rules->skipped) std::fprintf(stderr, "[trophy] %s: %s\n", rulesPath.c_str(), skipped.c_str()); + } + return title; +} + +const TitleTrophies& Title(std::uint32_t serviceLabel) { + static std::mutex mutex; + static std::map> titles; + std::lock_guard lock(mutex); + auto& title = titles[serviceLabel]; + if (!title) title = std::make_unique(LoadTitleTrophies(serviceLabel, LanguageTag(SystemServiceConsoleLanguage("libSceNpTrophy2")))); + return *title; +} + +const TrophyPackage* Package(std::uint32_t serviceLabel) { + const auto& title = Title(serviceLabel); + return title.package ? &*title.package : nullptr; +} + +void CopyText(char* destination, std::size_t size, std::string_view text) { + if (size == 0) throw std::invalid_argument("text cannot be copied into an empty buffer"); + auto length = std::min(text.size(), size - 1); + while (length > 0 && length < text.size() && (static_cast(text[length]) & 0xC0u) == 0x80u) --length; + std::memcpy(destination, text.data(), length); + destination[length] = '\0'; +} + +UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel) { + return [userId, serviceLabel](std::int32_t trophyId) { return TitleStore().UnlockTick(userId, serviceLabel, trophyId).has_value(); }; +} + +StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, StatStore& stats, std::int32_t userId, std::uint32_t serviceLabel) { + StatChanges changes; + const auto matching = rules.events.find(event.name); + if (matching == rules.events.end()) return changes; + for (const auto& rule : matching->second) { + const auto& definition = rules.stats.at(rule.statId); + const auto property = std::find_if(event.properties.begin(), event.properties.end(), [&rule](const NpTrophy2EventProperty& candidate) { return candidate.path == rule.input; }); + const auto where = event.name + " $." + rule.input + " for stat " + std::to_string(rule.statId); + if (property == event.properties.end()) { + changes.problems.push_back({where, "the event has no such property"}); + } else if (!property->value) { + changes.problems.push_back({where, property->text + " is not an unsigned integer"}); + } else if (*property->value < definition.minimum || *property->value > definition.maximum) { + changes.problems.push_back({where, property->text + " is not from " + std::to_string(definition.minimum) + " to " + std::to_string(definition.maximum)}); + } else { + stats.Set(userId, serviceLabel, rule.statId, *property->value); + changes.updates.push_back({rule.statId, *property->value, rule.input}); + } + } + return changes; +} + +std::vector ReachedTrophies(const TrophyPackage& package, const std::map& stats, const UnlockedQuery& unlocked) { + std::vector reached; + for (const auto& [id, trophy] : package.trophies) { + const auto condition = trophy.Condition(); + if (!condition) continue; + const auto stat = stats.find(condition->statId); + if (stat != stats.end() && condition->Reached(stat->second) && !unlocked(id)) reached.push_back(id); + } + return reached; +} + +std::optional PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked) { + const auto trophy = package.trophies.find(trophyId); + if (trophy == package.trophies.end() || !trophy->second.platinumId) return std::nullopt; + const auto platinumId = *trophy->second.platinumId; + const auto platinum = package.trophies.find(platinumId); + if (platinum == package.trophies.end() || platinum->second.grade != GradePlatinum || unlocked(platinumId)) return std::nullopt; + for (const auto& [id, other] : package.trophies) { + if (id != platinumId && other.platinumId == platinumId && !unlocked(id)) return std::nullopt; + } + return platinumId; +} + +std::optional PostedUnlockRefusal(const TrophyPackage& package, std::int32_t trophyId) { + const auto trophy = package.trophies.find(trophyId); + if (trophy == package.trophies.end()) return "the title's trophy data has no such trophy"; + if (trophy->second.grade == GradePlatinum) return "it is the platinum, which unlocks with the last trophy that names it"; + if (trophy->second.progressive) return "it is progressive and unlocks through its statistic"; + return std::nullopt; +} + +std::optional ProgressTarget(const TrophyDefinition& trophy) { + if (!trophy.progressive) return std::nullopt; + return trophy.target; +} + +std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional stat, bool unlocked) { + const auto target = ProgressTarget(trophy); + if (!target) throw std::invalid_argument("trophy " + std::to_string(trophy.id) + " is not progressive or has no target"); + if (unlocked) return *target; + return std::min(stat.value_or(0), *target); +} + +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional group, const UnlockedQuery& unlocked) { + TrophyCounts counts; + for (const auto& [id, trophy] : package.trophies) { + if (group && trophy.groupId != *group) continue; + ++counts.trophies; + ++counts.grades[static_cast(trophy.grade)]; + if (!unlocked(id)) continue; + ++counts.unlocked; + ++counts.unlockedGrades[static_cast(trophy.grade)]; + } + return counts; +} + +std::uint32_t TrophyCounts::Percentage() const { + const auto points = [](const std::array& counts) { return counts[GradeGold] * 6u + counts[GradeSilver] * 2u + counts[GradeBronze]; }; + const auto total = points(grades); + return total == 0 ? 0 : points(unlockedGrades) * 100u / total; +} + +} + +extern "C" void NpTrophy2ApplyEvent_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, const NpTrophy2Event& event) { + const auto& title = NpTrophy2::Title(serviceLabel); + if (!title.rules) return; + const auto changes = NpTrophy2::ApplyStatRules(*title.rules, event, NpTrophy2::TitleStats(), userId, serviceLabel); + for (const auto& problem : changes.problems) { + if (NpTrophy2::firstProblem(problem.rule)) std::fprintf(stderr, "[trophy] ignored %s: %s\n", problem.rule.c_str(), problem.reason.c_str()); + } + if (NpTrophy2::tracing()) { + for (const auto& update : changes.updates) { + std::fprintf(stderr, "[uds] stat %llu of user %d is %llu (%s $.%s)\n", static_cast(update.statId), userId, + static_cast(update.value), event.name.c_str(), update.input.c_str()); + } + } + if (changes.updates.empty() || !title.package) return; + auto stats = NpTrophy2::TitleStats().Values(userId, serviceLabel); + for (const auto& update : changes.updates) stats[update.statId] = update.value; + for (const auto trophyId : NpTrophy2::ReachedTrophies(*title.package, stats, NpTrophy2::UnlockedBy(userId, serviceLabel))) NpTrophy2::RecordUnlock(userId, serviceLabel, trophyId); +} diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index 5090940b58..ce29da1513 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -1,39 +1,92 @@ +#include #include #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void fillPlaceholderDetails(int trophyId, NpTrophy2Details* details) { + std::memset(details, 0, sizeof(*details)); + details->trophy_id = trophyId; + details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; + details->group_id = NP_TROPHY2_GROUP_ID_BASE; + details->hidden = false; + details->has_reward = false; + details->target.type = NP_TROPHY2_PROGRESS_TYPE_NONE; + details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; + std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1); + std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1); + std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); +} + +void fillPlaceholderData(int context, int trophyId, NpTrophy2Data* data) { + std::memset(data, 0, sizeof(*data)); + const auto tick = NpTrophy2::ContextUnlockTick(context, trophyId); + data->trophy_id = trophyId; + data->unlocked = tick.has_value(); + data->progress.type = NP_TROPHY2_PROGRESS_TYPE_NONE; + data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; + data->timestamp_tick = tick.value_or(0); +} + +void fillDetails(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Details* details) { + std::memset(details, 0, sizeof(*details)); + details->trophy_id = trophy.id; + details->trophy_grade = trophy.grade; + details->group_id = trophy.groupId; + details->hidden = trophy.hidden; + details->has_reward = trophy.hasReward; + if (const auto target = NpTrophy2::ProgressTarget(trophy)) { + details->target.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; + details->target.value = *target; + } + NpTrophy2::CopyText(details->name, sizeof(details->name), trophy.name); + NpTrophy2::CopyText(details->description, sizeof(details->description), trophy.detail); + NpTrophy2::CopyText(details->reward, sizeof(details->reward), trophy.reward); +} + +void fillData(const NpTrophy2::ContextOwner& owner, const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Data* data) { + std::memset(data, 0, sizeof(*data)); + const auto tick = NpTrophy2::TitleStore().UnlockTick(owner.userId, owner.serviceLabel, trophy.id); + data->trophy_id = trophy.id; + data->unlocked = tick.has_value(); + data->timestamp_tick = tick.value_or(0); + if (!NpTrophy2::ProgressTarget(trophy)) return; + const auto stat = trophy.statId ? NpTrophy2::TitleStats().Value(owner.userId, owner.serviceLabel, *trophy.statId) : std::nullopt; + data->progress.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; + data->progress.value = NpTrophy2::TrophyProgress(trophy, stat, data->unlocked); +} + +} + extern "C" { int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, NpTrophy2Details* details, NpTrophy2Data* data) { if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - if (details != nullptr) { - std::memset(details, 0, sizeof(*details)); - details->trophy_id = trophy_id; - details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; - details->group_id = NP_TROPHY2_GROUP_ID_BASE; - details->hidden = false; - details->has_reward = false; - details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1); - std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1); - std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + if (details != nullptr) fillPlaceholderDetails(trophy_id, details); + if (data != nullptr) fillPlaceholderData(context, trophy_id, data); + return SCE_NP_TROPHY2_OK; } - if (data != nullptr) { - std::memset(data, 0, sizeof(*data)); - data->trophy_id = trophy_id; - data->unlocked = false; - data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data->timestamp_tick = 0; + const auto trophy = package->trophies.find(trophy_id); + if (trophy == package->trophies.end()) { + throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } + if (details != nullptr) fillDetails(trophy->second, details); + if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; } @@ -41,28 +94,22 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int context, int handle, uint32_t o if (count == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); - *count = out_count; - if (out_count != 0 && details_array != nullptr) { - std::memset(details_array, 0, sizeof(*details_array)); - details_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT; - details_array->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; - details_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - details_array->hidden = false; - details_array->has_reward = false; - details_array->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - std::strncpy(details_array->name, NP_TROPHY2_TROPHY_NAME, sizeof(details_array->name) - 1); - std::strncpy(details_array->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details_array->description) - 1); - std::strncpy(details_array->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details_array->reward) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); + *count = out_count; + if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_TROPHY_ID_DEFAULT, details_array); + if (out_count != 0 && data_array != nullptr) fillPlaceholderData(context, NP_TROPHY2_TROPHY_ID_DEFAULT, data_array); + return SCE_NP_TROPHY2_OK; } - if (out_count != 0 && data_array != nullptr) { - std::memset(data_array, 0, sizeof(*data_array)); - data_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT; - data_array->unlocked = false; - data_array->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data_array->timestamp_tick = 0; + *count = 0; + auto trophy = package->trophies.begin(); + std::advance(trophy, std::min(offset, package->trophies.size())); + for (; trophy != package->trophies.end() && *count < limit; ++trophy, ++*count) { + if (details_array != nullptr) fillDetails(trophy->second, &details_array[*count]); + if (data_array != nullptr) fillData(*owner, trophy->second, &data_array[*count]); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp new file mode 100644 index 0000000000..5da39d1cb4 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp @@ -0,0 +1,592 @@ +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" + +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +constexpr int MaxJsonDepth = 64; +constexpr std::size_t MaxJsonValues = std::size_t{1} << 20u; + +std::uint64_t bigEndian(std::string_view data, std::uint64_t offset, std::size_t size) { + std::uint64_t value = 0; + for (std::size_t i = 0; i < size; ++i) value = (value << 8u) | static_cast(data[offset + i]); + return value; +} + +std::string hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +void appendUtf8(std::string& out, std::uint32_t code) { + if (code < 0x80) { + out += static_cast(code); + } else if (code < 0x800) { + out += static_cast(0xC0 | (code >> 6)); + out += static_cast(0x80 | (code & 0x3F)); + } else if (code < 0x10000) { + out += static_cast(0xE0 | (code >> 12)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } else { + out += static_cast(0xF0 | (code >> 18)); + out += static_cast(0x80 | ((code >> 12) & 0x3F)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } +} + +class JsonReader { +public: + explicit JsonReader(std::string_view text) : text(text) {} + + Json Document() { + if (text.starts_with("\xEF\xBB\xBF")) position = 3; + auto document = readValue(0); + skipSpace(); + if (position != text.size()) fail("text follows the document"); + return document; + } + +private: + [[noreturn]] void fail(const std::string& problem) const { + throw std::runtime_error(problem + " at byte " + std::to_string(position)); + } + + void skipSpace() { + while (position < text.size() && (text[position] == ' ' || text[position] == '\t' || text[position] == '\n' || text[position] == '\r')) ++position; + } + + bool take(char expected) { + if (position >= text.size() || text[position] != expected) return false; + ++position; + return true; + } + + void expect(std::string_view word) { + if (text.substr(position, word.size()) != word) fail("expected " + std::string(word)); + position += word.size(); + } + + Json readValue(int depth) { + if (depth >= MaxJsonDepth) fail("values nest deeper than " + std::to_string(MaxJsonDepth) + " levels"); + if (++values > MaxJsonValues) fail("the document holds more than " + std::to_string(MaxJsonValues) + " values"); + skipSpace(); + if (position >= text.size()) fail("the document ends before a value"); + Json result; + switch (text[position]) { + case '{': + result.kind = Json::Kind::Object; + readObject(result, depth); + break; + case '[': + result.kind = Json::Kind::Array; + readArray(result, depth); + break; + case '"': + result.kind = Json::Kind::String; + result.text = readString(); + break; + case 't': + expect("true"); + result.kind = Json::Kind::Bool; + result.boolean = true; + break; + case 'f': + expect("false"); + result.kind = Json::Kind::Bool; + break; + case 'n': + expect("null"); + break; + default: + result.kind = Json::Kind::Number; + result.text = readNumber(); + break; + } + return result; + } + + void readObject(Json& result, int depth) { + ++position; + skipSpace(); + if (take('}')) return; + while (true) { + skipSpace(); + if (position >= text.size() || text[position] != '"') fail("expected a member name"); + auto key = readString(); + skipSpace(); + if (!take(':')) fail("expected ':'"); + result.members.emplace_back(std::move(key), readValue(depth + 1)); + skipSpace(); + if (take('}')) return; + if (!take(',')) fail("expected ',' or '}'"); + } + } + + void readArray(Json& result, int depth) { + ++position; + skipSpace(); + if (take(']')) return; + while (true) { + result.items.push_back(readValue(depth + 1)); + skipSpace(); + if (take(']')) return; + if (!take(',')) fail("expected ',' or ']'"); + } + } + + std::uint32_t readHexDigits() { + if (text.size() - position < 4) fail("a \\u escape is cut short"); + std::uint32_t code = 0; + const auto* end = text.data() + position + 4; + const auto [stop, error] = std::from_chars(text.data() + position, end, code, 16); + if (error != std::errc{} || stop != end) fail("a \\u escape does not have 4 hex digits"); + position += 4; + return code; + } + + std::string readString() { + ++position; + std::string out; + while (true) { + if (position >= text.size()) fail("a string is not closed"); + const char character = text[position++]; + if (character == '"') return out; + if (static_cast(character) < 0x20) fail("a string holds a control character"); + if (character != '\\') { + out += character; + continue; + } + if (position >= text.size()) fail("a string is not closed"); + switch (text[position++]) { + case '"': out += '"'; break; + case '\\': out += '\\'; break; + case '/': out += '/'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case 'n': out += '\n'; break; + case 'r': out += '\r'; break; + case 't': out += '\t'; break; + case 'u': { + auto code = readHexDigits(); + if (code >= 0xDC00 && code < 0xE000) fail("a low surrogate has no high surrogate"); + if (code >= 0xD800 && code < 0xDC00) { + if (text.substr(position, 2) != "\\u") fail("a high surrogate has no low surrogate"); + position += 2; + const auto low = readHexDigits(); + if (low < 0xDC00 || low >= 0xE000) fail("a high surrogate has no low surrogate"); + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } + appendUtf8(out, code); + break; + } + default: + fail("a string has an unknown escape"); + } + } + } + + bool skipDigits() { + const auto start = position; + while (position < text.size() && text[position] >= '0' && text[position] <= '9') ++position; + return position != start; + } + + std::string readNumber() { + const auto start = position; + take('-'); + if (!take('0') && !skipDigits()) fail("expected a value"); + if (take('.') && !skipDigits()) fail("a number has no digits after '.'"); + if (take('e') || take('E')) { + if (!take('+')) take('-'); + if (!skipDigits()) fail("a number has no exponent digits"); + } + return std::string(text.substr(start, position - start)); + } + + std::string_view text; + std::size_t position = 0; + std::size_t values = 0; +}; + +std::string describe(const Json& value) { + switch (value.kind) { + case Json::Kind::Null: return "null"; + case Json::Kind::Bool: return value.boolean ? "true" : "false"; + case Json::Kind::Number: return value.text; + case Json::Kind::String: return "\"" + value.text + "\""; + case Json::Kind::Array: return "an array"; + case Json::Kind::Object: return "an object"; + } + throw std::logic_error("a JSON value has an unknown kind"); +} + +std::string kindName(Json::Kind kind) { + switch (kind) { + case Json::Kind::Null: return "null"; + case Json::Kind::Bool: return "boolean"; + case Json::Kind::Number: return "number"; + case Json::Kind::String: return "string"; + case Json::Kind::Array: return "array"; + case Json::Kind::Object: return "object"; + } + throw std::logic_error("a JSON value has an unknown kind"); +} + +const Json& member(const Json& object, std::string_view key, Json::Kind kind, const std::string& where) { + const auto* found = object.Find(key); + if (found == nullptr || found->kind != kind) throw std::runtime_error(where + " has no " + std::string(key) + " " + kindName(kind)); + return *found; +} + +Json document(const UcpFiles& files, const std::string& name) { + const auto found = files.find(name); + if (found == files.end()) throw std::runtime_error("it has no " + name); + Json parsed; + try { + parsed = ParseJson(found->second); + } catch (const std::runtime_error& error) { + throw std::runtime_error(name + " is not JSON: " + error.what()); + } + if (parsed.kind != Json::Kind::Object) throw std::runtime_error(name + " is not a JSON object"); + return parsed; +} + +Comparator comparatorOf(const Json& value) { + if (value.kind != Json::Kind::String) return Comparator::None; + if (value.text == "ge") return Comparator::GreaterEqual; + if (value.text == "gt") return Comparator::Greater; + if (value.text == "le") return Comparator::LessEqual; + if (value.text == "lt") return Comparator::Less; + return Comparator::None; +} + +std::string join(const std::vector& parts, std::string_view separator) { + std::string out; + for (const auto& part : parts) { + if (!out.empty()) out += separator; + out += part; + } + return out; +} + +std::string alternatives(const std::vector& parts) { + std::string out; + for (std::size_t index = 0; index < parts.size(); ++index) { + if (index != 0) out += index + 1 == parts.size() ? " or " : ", "; + out += parts[index]; + } + return out; +} + +TrophyDefinition readTrophy(const Json& definition, std::size_t index, std::vector& skipped) { + const auto where = "tropconf.json trophies[" + std::to_string(index) + "]"; + if (definition.kind != Json::Kind::Object) throw std::runtime_error(where + " is not an object"); + const auto* id = definition.Find("id"); + const auto parsedId = id != nullptr ? id->Id() : std::nullopt; + if (!parsedId) throw std::runtime_error(where + " has no id from 0 to 2147483647"); + TrophyDefinition trophy; + trophy.id = *parsedId; + const auto name = "tropconf.json trophy " + std::to_string(trophy.id); + const auto* grade = definition.Find("grade"); + const auto letter = grade != nullptr && grade->kind == Json::Kind::String && grade->text.size() == 1 ? std::string_view("PGSB").find(grade->text[0]) : std::string_view::npos; + if (letter == std::string_view::npos) throw std::runtime_error(name + " has no grade P, G, S or B"); + trophy.grade = GradePlatinum + static_cast(letter); + if (const auto* group = definition.Find("groupId")) { + const auto groupId = group->Id(); + if (!groupId) throw std::runtime_error(name + " has groupId " + describe(*group) + ", not an id from 0 to 2147483647"); + trophy.groupId = *groupId; + } + if (const auto* platinum = definition.Find("platinumTrophyId")) trophy.platinumId = platinum->Id(); + const auto* hidden = definition.Find("hidden"); + trophy.hidden = hidden != nullptr && hidden->IsTrue(); + const auto* reward = definition.Find("hasReward"); + trophy.hasReward = reward != nullptr && reward->IsTrue(); + const auto* condition = definition.Find("unlockCondition"); + if (condition == nullptr || condition->kind != Json::Kind::Object) return trophy; + const auto* progressive = condition->Find("progressive"); + trophy.progressive = progressive != nullptr && progressive->IsTrue(); + if (const auto* stat = condition->Find("udsStatId")) { + trophy.statId = stat->Unsigned(); + if (!trophy.statId) throw std::runtime_error(name + " has udsStatId " + describe(*stat) + ", not an unsigned integer"); + } + std::vector problems; + if (const auto* target = condition->Find("targetValue")) { + trophy.target = target->Unsigned(); + if (!trophy.target) problems.push_back("its targetValue " + describe(*target) + " is not an unsigned integer"); + } else if (trophy.statId) { + problems.emplace_back("it has no targetValue"); + } + if (const auto* comparator = condition->Find("comparator")) { + trophy.comparator = comparatorOf(*comparator); + if (trophy.comparator == Comparator::None) problems.push_back("its comparator " + describe(*comparator) + " is not ge, gt, le or lt"); + } else if (trophy.statId) { + problems.emplace_back("it has no comparator"); + } + if (!problems.empty()) skipped.push_back(name + " never unlocks through stats: " + join(problems, ", ")); + return trophy; +} + +std::optional propertyPath(std::string_view input) { + if (!input.starts_with("$.")) return std::nullopt; + const auto path = input.substr(2); + bool segmentStart = true; + for (const char character : path) { + if (character == '.') { + if (segmentStart) return std::nullopt; + segmentStart = true; + continue; + } + const bool word = (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_'; + if (!word) return std::nullopt; + segmentStart = false; + } + if (segmentStart) return std::nullopt; + return std::string(path); +} + +void readStat(const Json& definition, std::size_t index, StatRules& rules) { + const auto* id = definition.Find("statId"); + const auto statId = id != nullptr ? id->Unsigned() : std::nullopt; + if (!statId) { + rules.skipped.push_back("stats_definition.json statDefinitionArray[" + std::to_string(index) + "] has no unsigned statId"); + return; + } + const auto name = "stat " + std::to_string(*statId); + const auto* aggregation = definition.Find("aggregation"); + if (aggregation == nullptr || aggregation->kind != Json::Kind::String || aggregation->text != "latest") { + rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" is supported"); + return; + } + const auto* type = definition.Find("dataType"); + StatDefinition stat; + if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint32") { + stat.type = StatType::UInt32; + stat.maximum = UINT32_MAX; + } else if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint64") { + stat.type = StatType::UInt64; + stat.maximum = UINT64_MAX; + } else { + rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\" and \"uint64\" are supported"); + return; + } + if (const auto* minimum = definition.Find("minValue")) { + const auto value = minimum->Unsigned(); + if (!value || *value > stat.maximum) { + rules.skipped.push_back(name + " has minValue " + describe(*minimum) + ", which its " + type->text + " type cannot hold"); + return; + } + stat.minimum = *value; + } + if (const auto* maximum = definition.Find("maxValue")) { + const auto value = maximum->Unsigned(); + if (!value || *value > stat.maximum || *value < stat.minimum) { + rules.skipped.push_back(name + " has maxValue " + describe(*maximum) + ", which is not from its minValue to its " + type->text + " limit"); + return; + } + stat.maximum = *value; + } + if (!rules.stats.emplace(*statId, stat).second) rules.skipped.push_back(name + " is defined twice; the first definition is used"); +} + +void readRule(const Json& rule, std::size_t index, StatRules& rules) { + const auto where = "stats_extraction.json statsExtractionRuleArray[" + std::to_string(index) + "]"; + const auto* condition = rule.Find("condition"); + const auto* action = rule.Find("action"); + if (condition == nullptr || condition->kind != Json::Kind::Object || action == nullptr || action->kind != Json::Kind::Object) { + rules.skipped.push_back(where + " has no condition and action objects"); + return; + } + const auto* event = condition->Find("eventName"); + if (event == nullptr || event->kind != Json::Kind::String) { + rules.skipped.push_back(where + " has no eventName"); + return; + } + if (condition->members.size() != 1) { + rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only eventName conditions are supported"); + return; + } + const auto* output = action->Find("output"); + const auto* id = output != nullptr ? output->Find("statId") : nullptr; + const auto statId = id != nullptr ? id->Unsigned() : std::nullopt; + if (!statId) { + rules.skipped.push_back(where + " for " + event->text + " has no unsigned output statId"); + return; + } + if (!rules.stats.contains(*statId)) { + rules.skipped.push_back(where + " for " + event->text + " writes stat " + std::to_string(*statId) + ", which has no supported definition"); + return; + } + const auto* input = action->Find("input"); + const auto path = input != nullptr && input->kind == Json::Kind::String ? propertyPath(input->text) : std::nullopt; + if (!path) { + rules.skipped.push_back(where + " for " + event->text + " reads " + (input != nullptr ? describe(*input) : std::string("no input")) + "; only \"$.name\" property paths are supported"); + return; + } + rules.events[event->text].push_back({*path, *statId}); +} + +} + +const Json* Json::Find(std::string_view key) const { + for (auto member = members.rbegin(); member != members.rend(); ++member) { + if (member->first == key) return &member->second; + } + return nullptr; +} + +bool Json::IsTrue() const { + return kind == Kind::Bool && boolean; +} + +std::optional Json::Unsigned() const { + if (kind != Kind::Number && kind != Kind::String) return std::nullopt; + std::uint64_t value = 0; + const auto* end = text.data() + text.size(); + const auto [stop, error] = std::from_chars(text.data(), end, value); + if (error != std::errc{} || stop != end) return std::nullopt; + return value; +} + +std::optional Json::Id() const { + const auto value = Unsigned(); + if (!value || *value > static_cast(INT32_MAX)) return std::nullopt; + return static_cast(*value); +} + +Json ParseJson(std::string_view text) { + return JsonReader(text).Document(); +} + +UcpFiles ReadUcp(std::string_view data) { + if (data.size() < UcpHeaderSize) throw std::runtime_error("it has " + std::to_string(data.size()) + " bytes, fewer than the " + std::to_string(UcpHeaderSize) + "-byte header"); + const auto magic = bigEndian(data, 0, 4); + if (magic != UcpMagic) throw std::runtime_error("its magic is " + hex(magic) + ", not " + hex(UcpMagic)); + const auto version = bigEndian(data, 4, 4); + if (version != UcpVersion) throw std::runtime_error("its version is " + std::to_string(version) + ", not " + std::to_string(UcpVersion)); + const auto size = bigEndian(data, 8, 8); + if (size < UcpHeaderSize || size > data.size()) throw std::runtime_error("its size " + hex(size) + " is not from the header size " + hex(UcpHeaderSize) + " to the file size " + hex(data.size())); + const auto count = bigEndian(data, 0x10, 4); + if (count > UcpMaxEntries) throw std::runtime_error("it lists " + std::to_string(count) + " files, more than " + std::to_string(UcpMaxEntries)); + const auto toc = bigEndian(data, 0x14, 4); + if (toc > size || UcpTocHeaderSize + count * UcpEntrySize > size - toc) { + throw std::runtime_error("its table of " + std::to_string(count) + " files at " + hex(toc) + " ends past its size " + hex(size)); + } + UcpFiles files; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = toc + UcpTocHeaderSize + index * UcpEntrySize; + const auto field = data.substr(entry, UcpNameSize); + const std::string name(field.substr(0, field.find('\0'))); + const auto offset = bigEndian(data, entry + UcpNameSize, 8); + const auto length = bigEndian(data, entry + UcpNameSize + 8, 8); + if (offset > size || length > size - offset) { + throw std::runtime_error("its file \"" + name + "\" at " + hex(offset) + " with " + hex(length) + " bytes ends past its size " + hex(size)); + } + files.try_emplace(name, data.substr(offset, length)); + } + return files; +} + +bool UnlockCondition::Reached(std::uint64_t value) const { + switch (comparator) { + case Comparator::GreaterEqual: return value >= target; + case Comparator::Greater: return value > target; + case Comparator::LessEqual: return value <= target; + case Comparator::Less: return value < target; + case Comparator::None: break; + } + throw std::logic_error("an unlock condition on stat " + std::to_string(statId) + " has no comparator"); +} + +std::optional TrophyDefinition::Condition() const { + if (grade == GradePlatinum || !statId || !target || comparator == Comparator::None) return std::nullopt; + return UnlockCondition{*statId, *target, comparator}; +} + +TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language) { + const auto files = ReadUcp(ucp); + const auto conf = document(files, "tropconf.json"); + const auto& trophies = member(conf, "trophies", Json::Kind::Array, "tropconf.json"); + if (trophies.items.size() > MaxTrophies) throw std::runtime_error("tropconf.json lists " + std::to_string(trophies.items.size()) + " trophies, more than " + std::to_string(MaxTrophies)); + const auto& defaultLanguage = member(conf, "defaultLanguage", Json::Kind::String, "tropconf.json"); + TrophyPackage package; + const std::array candidates{language, "en-US", defaultLanguage.text}; + std::vector tried; + for (std::size_t index = 0; index < candidates.size() && package.language.empty(); ++index) { + const auto candidate = candidates[index]; + const auto earlier = candidates.begin() + static_cast(index); + if (candidate.empty() || std::find(candidates.begin(), earlier, candidate) != earlier) continue; + if (files.contains("tropmeta_" + std::string(candidate) + ".json")) { + package.language = candidate; + } else { + tried.push_back(index + 1 == candidates.size() ? "its defaultLanguage \"" + std::string(candidate) + "\"" : std::string(candidate)); + } + } + if (package.language.empty()) throw std::runtime_error("it has no tropmeta_.json for " + alternatives(tried)); + const auto metaName = "tropmeta_" + package.language + ".json"; + const auto meta = document(files, metaName); + const auto& metadata = member(meta, "metadata", Json::Kind::Object, metaName); + const auto& titleMetadata = member(metadata, "titleMetadata", Json::Kind::Object, metaName + " metadata"); + package.title = member(titleMetadata, "name", Json::Kind::String, metaName + " titleMetadata").text; + const auto& trophyMetadata = member(metadata, "trophyMetadata", Json::Kind::Array, metaName + " metadata"); + for (std::size_t index = 0; index < trophies.items.size(); ++index) { + auto trophy = readTrophy(trophies.items[index], index, package.skipped); + package.groups.try_emplace(trophy.groupId, trophy.groupId == BaseGroup ? package.title : std::string()); + if (package.groups.size() > MaxGroups) throw std::runtime_error("tropconf.json uses more than " + std::to_string(MaxGroups) + " groups"); + const auto id = trophy.id; + if (!package.trophies.emplace(id, std::move(trophy)).second) throw std::runtime_error("tropconf.json lists trophy " + std::to_string(id) + " twice"); + } + if (const auto* groups = metadata.Find("groupMetadata"); groups != nullptr && groups->kind == Json::Kind::Array) { + for (const auto& group : groups->items) { + const auto* id = group.Find("id"); + const auto* name = group.Find("name"); + const auto groupId = id != nullptr ? id->Id() : std::nullopt; + if (!groupId || name == nullptr || name->kind != Json::Kind::String) continue; + if (const auto found = package.groups.find(*groupId); found != package.groups.end()) found->second = name->text; + } + } + for (const auto& text : trophyMetadata.items) { + const auto* id = text.Find("id"); + const auto trophyId = id != nullptr ? id->Id() : std::nullopt; + const auto found = trophyId ? package.trophies.find(*trophyId) : package.trophies.end(); + if (found == package.trophies.end()) continue; + for (const auto& [key, field] : {std::pair{"name", &found->second.name}, std::pair{"detail", &found->second.detail}, std::pair{"reward", &found->second.reward}}) { + if (const auto* value = text.Find(key); value != nullptr && value->kind == Json::Kind::String) *field = value->text; + } + } + return package; +} + +StatRules ParseStatRules(std::string_view ucp) { + const auto files = ReadUcp(ucp); + const auto definitions = document(files, "stats_definition.json"); + const auto extraction = document(files, "stats_extraction.json"); + const auto& stats = member(definitions, "statDefinitionArray", Json::Kind::Array, "stats_definition.json"); + const auto& extractionRules = member(extraction, "statsExtractionRuleArray", Json::Kind::Array, "stats_extraction.json"); + StatRules rules; + for (std::size_t index = 0; index < stats.items.size(); ++index) readStat(stats.items[index], index, rules); + for (std::size_t index = 0; index < extractionRules.items.size(); ++index) readRule(extractionRules.items[index], index, rules); + return rules; +} + +std::string_view LanguageTag(int consoleLanguage) { + static constexpr std::array tags = { + "ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant", + "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO", "pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", + "fr-CA", "cs-CZ", "hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID", "uk-UA"}; + if (consoleLanguage < 0 || consoleLanguage >= static_cast(tags.size())) { + throw std::out_of_range("console language " + std::to_string(consoleLanguage) + " has no locale tag"); + } + return tags[static_cast(consoleLanguage)]; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp new file mode 100644 index 0000000000..b19804fe10 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -0,0 +1,260 @@ +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +template +bool parseNumber(std::string_view text, TValue& value) { + const auto* end = text.data() + text.size(); + const auto [stop, error] = std::from_chars(text.data(), end, value); + return error == std::errc{} && stop == end; +} + +template +bool negative(TKey key) { + if constexpr (std::is_signed_v) return key < 0; + return false; +} + +template +std::map readRecord(const std::filesystem::path& path, const std::string& what, const char* pair) { + std::map values; + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) throw std::filesystem::filesystem_error("cannot check the " + what, path, error); + if (!exists) return values; + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("cannot read the " + what + " " + path.string()); + std::string line; + std::size_t number = 0; + while (std::getline(file, line)) { + ++number; + const auto space = line.find(' '); + TKey key{}; + std::uint64_t value = 0; + const std::string_view text(line); + if (space == std::string::npos || !parseNumber(text.substr(0, space), key) || negative(key) || !parseNumber(text.substr(space + 1), value) || !values.emplace(key, value).second) { + throw std::runtime_error("the " + what + " " + path.string() + " has a line " + std::to_string(number) + " that is not a new '" + pair + "' pair"); + } + } + if (file.bad()) throw std::runtime_error("cannot read the " + what + " " + path.string()); + return values; +} + +template +void writeRecord(const std::filesystem::path& directory, const std::filesystem::path& path, const std::map& values, const std::string& what) { + std::error_code error; + if (std::filesystem::create_directories(directory, error)) RecordWrittenPath_nid_no_patch(directory); + if (error) throw std::filesystem::filesystem_error("cannot create the trophy directory", directory, error); + auto temporary = path; + temporary += ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + for (const auto& [key, value] : values) file << key << ' ' << value << '\n'; + file.flush(); + if (!file) throw std::runtime_error("cannot write the " + what + " " + temporary.string()); + } + std::filesystem::rename(temporary, path); + RecordWrittenPath_nid_no_patch(path); +} + +std::string recordName(std::int32_t userId, std::uint32_t serviceLabel) { + return std::to_string(userId) + "_" + std::to_string(serviceLabel); +} + +struct Listener { + std::uint64_t id = 0; + NpTrophy2UnlockListener callback = nullptr; + void* context = nullptr; +}; + +struct Listeners { + std::mutex mutex; + std::recursive_mutex dispatch; + std::vector entries; + std::uint64_t next = 1; +}; + +bool registered(Listeners& registry, std::uint64_t id) { + std::lock_guard lock(registry.mutex); + return std::any_of(registry.entries.begin(), registry.entries.end(), [id](const Listener& entry) { return entry.id == id; }); +} + +Listeners& listeners() { + static Listeners registry; + return registry; +} + +} + +std::uint64_t CurrentTick() { + const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + return UnixEpochTick + static_cast(now); +} + +TrophyStore::TrophyStore(std::filesystem::path directory) : directory(std::move(directory)) {} + +std::filesystem::path TrophyStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { + return directory / (recordName(userId, serviceLabel) + ".txt"); +} + +TrophyStore::Unlocks& TrophyStore::record(const Key& key) { + if (const auto found = records.find(key); found != records.end()) return found->second; + auto unlocks = readRecord(RecordPath(key.first, key.second), "trophy record", " "); + return records.emplace(key, std::move(unlocks)).first->second; +} + +void TrophyStore::save(const Key& key, const Unlocks& unlocks) const { + writeRecord(directory, RecordPath(key.first, key.second), unlocks, "trophy record"); +} + +bool TrophyStore::Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick) { + if (trophyId < 0) throw std::invalid_argument("trophy " + std::to_string(trophyId) + " cannot be unlocked: trophy ids are not negative"); + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& unlocks = record(key); + if (unlocks.contains(trophyId)) return false; + auto updated = unlocks; + updated.emplace(trophyId, tick); + save(key, updated); + unlocks = std::move(updated); + return true; +} + +std::optional TrophyStore::UnlockTick(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + std::lock_guard lock(mutex); + const auto& unlocks = record({userId, serviceLabel}); + const auto found = unlocks.find(trophyId); + if (found == unlocks.end()) return std::nullopt; + return found->second; +} + +std::size_t TrophyStore::UnlockCount(std::int32_t userId, std::uint32_t serviceLabel) { + std::lock_guard lock(mutex); + return record({userId, serviceLabel}).size(); +} + +TrophyStore& TitleStore() { + static TrophyStore store(ResolvePath_nid_no_patch(TrophyRootGuest)); + return store; +} + +StatStore::StatStore(std::filesystem::path directory) : directory(std::move(directory)) {} + +std::filesystem::path StatStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { + return directory / (recordName(userId, serviceLabel) + ".stats.txt"); +} + +StatStore::Stats& StatStore::record(const Key& key) { + if (const auto found = records.find(key); found != records.end()) return found->second; + auto stats = readRecord(RecordPath(key.first, key.second), "stat record", " "); + return records.emplace(key, std::move(stats)).first->second; +} + +void StatStore::Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value) { + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& stats = record(key); + if (const auto found = stats.find(statId); found != stats.end() && found->second == value) return; + auto updated = stats; + updated[statId] = value; + writeRecord(directory, RecordPath(userId, serviceLabel), updated, "stat record"); + stats = std::move(updated); +} + +std::optional StatStore::Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId) { + std::lock_guard lock(mutex); + const auto& stats = record({userId, serviceLabel}); + const auto found = stats.find(statId); + if (found == stats.end()) return std::nullopt; + return found->second; +} + +std::map StatStore::Values(std::int32_t userId, std::uint32_t serviceLabel) { + std::lock_guard lock(mutex); + return record({userId, serviceLabel}); +} + +StatStore& TitleStats() { + static StatStore store(ResolvePath_nid_no_patch(TrophyRootGuest)); + return store; +} + +std::optional ContextUnlockTick(int context, std::int32_t trophyId) { + const auto owner = FindContext(context); + if (!owner || trophyId < 0) return std::nullopt; + return TitleStore().UnlockTick(owner->userId, owner->serviceLabel, trophyId); +} + +std::size_t ContextUnlockCount(int context) { + const auto owner = FindContext(context); + return owner ? TitleStore().UnlockCount(owner->userId, owner->serviceLabel) : 0; +} + +bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + const auto title = GetAppTitleId_nid_postfix(); + const std::string_view titleId(title.value, static_cast(std::find(std::begin(title.value), std::end(title.value), '\0') - std::begin(title.value))); + const NpTrophy2Unlock unlock{std::string(titleId), userId, serviceLabel, trophyId, CurrentTick()}; + if (!TitleStore().Unlock(userId, serviceLabel, trophyId, unlock.tick)) return false; + std::fprintf(stderr, "[trophy] %s unlocked trophy %d for user %d\n", unlock.titleId.c_str(), trophyId, userId); + auto& registry = listeners(); + std::lock_guard dispatching(registry.dispatch); + std::vector current; + { + std::lock_guard lock(registry.mutex); + current = registry.entries; + } + for (const auto& entry : current) { + if (registered(registry, entry.id)) entry.callback(entry.context, unlock); + } + const auto* package = Package(serviceLabel); + if (package == nullptr) return true; + if (const auto platinum = PlatinumToUnlock(*package, trophyId, UnlockedBy(userId, serviceLabel))) RecordUnlock(userId, serviceLabel, *platinum); + return true; +} + +} + +extern "C" bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + if (const auto* package = NpTrophy2::Package(serviceLabel)) { + if (const auto refusal = NpTrophy2::PostedUnlockRefusal(*package, trophyId)) { + std::fprintf(stderr, "[trophy] ignored the posted unlock of trophy %d for user %d: %s\n", trophyId, userId, refusal->c_str()); + return false; + } + } + return NpTrophy2::RecordUnlock(userId, serviceLabel, trophyId); +} + +extern "C" std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context) { + if (listener == nullptr) { + APS5_INVALID_ARG_EX; + } + auto& registry = NpTrophy2::listeners(); + std::lock_guard lock(registry.mutex); + const auto id = registry.next++; + registry.entries.push_back({id, listener, context}); + return id; +} + +extern "C" void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id) { + auto& registry = NpTrophy2::listeners(); + std::lock_guard dispatching(registry.dispatch); + std::lock_guard lock(registry.mutex); + const auto removed = std::erase_if(registry.entries, [id](const NpTrophy2::Listener& entry) { return entry.id == id; }); + if (removed == 0) throw std::invalid_argument("no trophy unlock listener has id " + std::to_string(id)); +} diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp new file mode 100644 index 0000000000..d1dcdba734 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -0,0 +1,434 @@ +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Files = std::vector>; + +constexpr std::int32_t InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +std::string failure(TAction action) { + try { + action(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + +bool contains(const std::string& text, const char* part) { + return text.find(part) != std::string::npos; +} + +void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +std::string ucp(const Files& files) { + const std::size_t toc = 0x40; + std::size_t offset = toc + 0x20 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& [name, contents] : files) size += contents.size(); + std::string data(size, '\0'); + putBigEndian(data, 0, 0xB228C60A, 4); + putBigEndian(data, 4, 1, 4); + putBigEndian(data, 8, size, 8); + putBigEndian(data, 0x10, files.size(), 4); + putBigEndian(data, 0x14, toc, 4); + std::size_t entry = toc + 0x20; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + putBigEndian(data, entry + 0x20, offset, 8); + putBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P","hidden":false}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":0,"unlockCondition":{"udsStatId":20,"comparator":"gt","targetValue":100}}, + {"id":3,"grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true,"platinumTrophyId":"-1"}, + {"id":"7","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"10","comparator":"eq","targetValue":"-1"}} +]})"; + +const char TropMetaEnglish[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them","detail":"Unlock every trophy"},{"id":"1","name":"Ten","detail":"Reach ten"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"},{"id":"99","name":"Not a trophy"}], + "groupMetadata":[{"id":"1","name":"Expansion"},{"id":"9","name":"Unused"}]}})"; + +const char TropMetaFrench[] = R"({"metadata":{"titleMetadata":{"name":"Jeu synth\u00e9tique"},"trophyMetadata":[{"id":"1","name":"Dix"}]}})"; + +const char StatsDefinition[] = R"({"statDefinitionArray":[ + {"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"}, + {"statId":20,"dataType":"uint64","aggregation":"latest"}, + {"statId":30,"dataType":"uint32","aggregation":"latest"}, + {"statId":"40","dataType":"uint32","aggregation":"latest","maxValue":"50"}, + {"statId":50,"dataType":"uint64","aggregation":"sum"}, + {"statId":60,"dataType":"int32","aggregation":"latest"}, + {"statId":70,"dataType":"uint32","aggregation":"latest","maxValue":"5000000000"}, + {"dataType":"uint32","aggregation":"latest"} +]})"; + +const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, + {"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}}, + {"condition":{"eventName":"STEPS"},"action":{"input":"$.player.steps","output":{"statId":"40"}}}, + {"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":50}}}, + {"condition":{"eventName":"FILTERED","property":{"path":"$.level"}},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COUNT"},"action":{"output":{"statId":10}}}, + {"condition":{"eventName":"BRACKETS"},"action":{"input":"$['counter']","output":{"statId":10}}}, + {"bad":1} +]})"; + +std::string trophyPackage() { + return ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMetaEnglish}, {"tropmeta_fr-FR.json", TropMetaFrench}, {"trop0001.png", "not an image"}}); +} + +std::string statPackage() { + return ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", StatsExtraction}}); +} + +void testContainer() { + const auto data = ucp({{"a.json", "{}"}, {"b.bin", std::string("\0\1\2", 3)}, {"a.json", "second"}}); + const auto files = NpTrophy2::ReadUcp(data); + check(files.size() == 2 && files.at("a.json") == "{}" && files.at("b.bin") == std::string_view("\0\1\2", 3), "the container files were not read, or a repeated name replaced the first"); + check(NpTrophy2::ReadUcp(ucp({})).empty(), "an empty container was not read"); + const std::string longName(0x20, 'n'); + const auto named = NpTrophy2::ReadUcp(ucp({{longName, "x"}})); + check(named.size() == 1 && named.begin()->first == longName, "a name that fills its 32 bytes was not read whole"); + auto padded = data + "trailing"; + check(NpTrophy2::ReadUcp(padded).size() == 2, "bytes after the container size were not ignored"); + + for (std::size_t length = 0; length < data.size(); ++length) { + check(!failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, length)); }).empty(), "a truncated container was read"); + } + check(contains(failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, 0x3F)); }), "fewer than the 64-byte header"), "a short header was not named"); + auto corrupt = data; + corrupt[0] = 0; + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "magic is 0x28c60a"), "a wrong magic was not named"); + corrupt = data; + putBigEndian(corrupt, 4, 2, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "version is 2"), "a wrong version was not named"); + corrupt = data; + putBigEndian(corrupt, 8, 0x3F, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its size 0x3f"), "a size inside the header was accepted"); + corrupt = data; + putBigEndian(corrupt, 8, data.size() + 1, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "is not from the header size"), "a size past the file was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x10, 4097, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "more than 4096"), "too many files were accepted"); + corrupt = data; + putBigEndian(corrupt, 0x10, 40, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "table of 40 files"), "a table past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x14, data.size() + 1, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a table offset past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x60 + 0x20, data.size() + 1, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its file \"a.json\""), "a file offset past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x60 + 0x28, ~std::uint64_t{0}, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a file length past the size was accepted"); +} + +void testJson() { + const auto value = NpTrophy2::ParseJson("\xEF\xBB\xBF {\"a\": [1, -2.5e+3, \"x\\u00e9\\ud83d\\ude00\\n\", true, false, null, {}], \"a\": 7, \"b\": \"12\"} "); + check(value.kind == NpTrophy2::Json::Kind::Object && value.members.size() == 3, "an object was not parsed"); + check(value.Find("a") != nullptr && value.Find("a")->text == "7", "a repeated key did not keep its last value"); + const auto& list = value.members[0].second.items; + check(list.size() == 7 && list[0].text == "1" && list[1].text == "-2.5e+3" && list[2].text == "x\xC3\xA9\xF0\x9F\x98\x80\n", "array values or escapes were not parsed"); + check(list[3].IsTrue() && !list[4].IsTrue() && list[5].kind == NpTrophy2::Json::Kind::Null && list[6].kind == NpTrophy2::Json::Kind::Object, "literals were not parsed"); + check(value.Find("b")->Unsigned() == std::optional(12) && list[0].Unsigned() == std::optional(1), "numbers and numeric strings are not unsigned"); + check(!list[1].Unsigned() && !list[3].Unsigned() && !NpTrophy2::ParseJson("\"\"").Unsigned() && !NpTrophy2::ParseJson("\" 1\"").Unsigned(), "a value that is not an unsigned integer was read as one"); + check(NpTrophy2::ParseJson("18446744073709551615").Unsigned() == std::optional(UINT64_MAX) && !NpTrophy2::ParseJson("18446744073709551616").Unsigned(), "the unsigned range is wrong"); + check(NpTrophy2::ParseJson("2147483647").Id() == std::optional(INT32_MAX) && !NpTrophy2::ParseJson("2147483648").Id() && !NpTrophy2::ParseJson("-1").Id(), "the id range is wrong"); + std::string deep(64, '['); + deep += std::string(64, ']'); + check(NpTrophy2::ParseJson(deep).kind == NpTrophy2::Json::Kind::Array, "64 levels of nesting were refused"); + for (const char* text : {"", "{", "[1,]", "{\"a\":1,}", "{\"a\" 1}", "{a:1}", "01", "1.", "-", "1e", "+1", "\"\\x\"", "\"\\u12g4\"", "\"\\ud800\"", "\"\\udc00\"", + "\"\\ud800\\u0041\"", "\"a\nb\"", "\"open", "tru", "nul", "{} {}", "[1] x"}) { + check(!failure([text] { NpTrophy2::ParseJson(text); }).empty(), "malformed JSON was parsed"); + } + const auto deeper = "[" + deep + "]"; + check(contains(failure([&deeper] { NpTrophy2::ParseJson(deeper); }), "deeper than 64"), "deep nesting was not refused"); + std::string most = "["; + for (std::size_t value = 2; value < 1048576; ++value) most += "0,"; + most += "0]"; + check(NpTrophy2::ParseJson(most).items.size() == 1048575, "a document of 1048576 values was refused"); + const auto more = "[0," + most.substr(1); + check(contains(failure([&more] { NpTrophy2::ParseJson(more); }), "holds more than 1048576 values"), "a document of more than 1048576 values was parsed"); +} + +void testPackage() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + check(package.language == "en-US" && package.title == "Synthetic Game" && package.trophies.size() == 8, "the package was not read"); + const auto& platinum = package.trophies.at(0); + check(platinum.grade == NpTrophy2::GradePlatinum && platinum.name == "All of them" && platinum.detail == "Unlock every trophy" && !platinum.Condition(), "the platinum was not read"); + const auto& gold = package.trophies.at(1); + check(gold.grade == NpTrophy2::GradeGold && gold.name == "Ten" && gold.platinumId == std::optional(0) && gold.groupId == NpTrophy2::BaseGroup, "trophy 1 was not read"); + const auto condition = gold.Condition(); + check(condition && condition->statId == 10 && condition->target == 10 && condition->comparator == NpTrophy2::Comparator::GreaterEqual && !gold.progressive, "trophy 1's condition was not read"); + check(package.trophies.at(2).grade == NpTrophy2::GradeSilver && package.trophies.at(3).grade == NpTrophy2::GradeBronze, "grades were not read"); + check(package.trophies.at(2).Condition()->comparator == NpTrophy2::Comparator::Greater && package.trophies.at(3).Condition()->comparator == NpTrophy2::Comparator::LessEqual && + package.trophies.at(4).Condition()->comparator == NpTrophy2::Comparator::Less, + "comparators were not read"); + const auto& progressive = package.trophies.at(5); + check(progressive.progressive && NpTrophy2::ProgressTarget(progressive) == std::optional(5) && !NpTrophy2::ProgressTarget(gold), "the progressive target was not read"); + const auto& hidden = package.trophies.at(6); + check(hidden.hidden && hidden.hasReward && hidden.groupId == 1 && hidden.name == "Secret" && hidden.reward == "A hat" && !hidden.platinumId, "trophy 6 was not read"); + check(!package.trophies.at(7).Condition() && package.trophies.at(7).name.empty(), "an unusable condition was kept"); + check(package.skipped.size() == 1 && contains(package.skipped[0], "trophy 7 never unlocks through stats") && contains(package.skipped[0], "\"-1\"") && contains(package.skipped[0], "\"eq\""), + "the unusable condition was not reported"); + check(package.groups.size() == 2 && package.groups.at(NpTrophy2::BaseGroup) == "Synthetic Game" && package.groups.at(1) == "Expansion", "groups were not read"); + + const auto french = NpTrophy2::ParseTrophyPackage(trophyPackage(), "fr-FR"); + check(french.language == "fr-FR" && french.title == "Jeu synth\xC3\xA9tique" && french.trophies.at(1).name == "Dix" && french.trophies.at(6).name.empty(), "the system language was not used"); + check(NpTrophy2::ParseTrophyPackage(trophyPackage(), "de-DE").language == "en-US", "English was not the fallback"); + std::string conf = TropConf; + conf.replace(conf.find("en-US"), 5, "ja-JP"); + check(NpTrophy2::ParseTrophyPackage(ucp({{"tropconf.json", conf}, {"tropmeta_ja-JP.json", TropMetaEnglish}}), "de-DE").language == "ja-JP", "the default language was not the last fallback"); + const auto withoutTexts = ucp({{"tropconf.json", conf}}); + check(contains(failure([&withoutTexts] { NpTrophy2::ParseTrophyPackage(withoutTexts, "de-DE"); }), "no tropmeta_.json for de-DE, en-US or its defaultLanguage \"ja-JP\""), + "a package without texts was read"); + const auto missingTexts = [](const std::string& defaultLanguage, const char* language) { + std::string text = TropConf; + text.replace(text.find("en-US"), 5, defaultLanguage); + const auto data = ucp({{"tropconf.json", text}}); + return failure([&data, language] { NpTrophy2::ParseTrophyPackage(data, language); }); + }; + check(missingTexts("en-US", "en-US") == "it has no tropmeta_.json for en-US", "a missing text named en-US more than once"); + check(missingTexts("ja-JP", "en-US") == "it has no tropmeta_.json for en-US or its defaultLanguage \"ja-JP\"", "a missing text in English named English twice"); + check(missingTexts("en-US", "de-DE") == "it has no tropmeta_.json for de-DE or en-US", "a missing text with an English default named English twice"); + + const auto broken = [](const std::string& replaced, const std::string& replacement) { + std::string text = TropConf; + text.replace(text.find(replaced), replaced.size(), replacement); + const auto data = ucp({{"tropconf.json", text}, {"tropmeta_en-US.json", TropMetaEnglish}}); + return failure([&data] { NpTrophy2::ParseTrophyPackage(data, "en-US"); }); + }; + check(contains(broken(R"("grade":"G")", R"("grade":"X")"), "trophy 1 has no grade"), "a wrong grade was accepted"); + check(contains(broken(R"("grade":"G")", R"("grade":"GS")"), "trophy 1 has no grade"), "a two-letter grade was accepted"); + check(contains(broken(R"({"id":"4",)", R"({"id":"3",)"), "lists trophy 3 twice"), "a repeated trophy was accepted"); + check(contains(broken(R"({"id":"4",)", R"({"id":"-4",)"), "trophies[4] has no id"), "a negative id was accepted"); + check(contains(broken(R"("groupId":"1","hidden")", R"("groupId":"x","hidden")"), "trophy 6 has groupId \"x\""), "a wrong group was accepted"); + check(contains(broken(R"("udsStatId":"10")", R"("udsStatId":"-10")"), "udsStatId \"-10\""), "a negative stat id was accepted"); + check(contains(broken(R"("trophies":[)", R"("trophies":0,"other":[)"), "has no trophies array"), "trophies that are not an array were accepted"); + const auto withoutConf = ucp({{"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&withoutConf] { NpTrophy2::ParseTrophyPackage(withoutConf, "en-US"); }), "no tropconf.json"), "a package without tropconf.json was read"); + const auto brokenConf = ucp({{"tropconf.json", "{\"trophies\":["}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&brokenConf] { NpTrophy2::ParseTrophyPackage(brokenConf, "en-US"); }), "tropconf.json is not JSON"), "a tropconf.json that is not JSON was read"); + const auto withoutTitle = ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", "{\"metadata\":{}}"}}); + check(contains(failure([&withoutTitle] { NpTrophy2::ParseTrophyPackage(withoutTitle, "en-US"); }), "has no titleMetadata"), "texts without a title were read"); + std::string groups = R"({"defaultLanguage":"en-US","trophies":[)"; + for (int id = 0; id <= 50; ++id) groups += (id == 0 ? "" : ",") + std::string(R"({"id":)") + std::to_string(id) + R"(,"grade":"B","groupId":)" + std::to_string(id) + "}"; + groups += "]}"; + const auto manyGroups = ucp({{"tropconf.json", groups}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&manyGroups] { NpTrophy2::ParseTrophyPackage(manyGroups, "en-US"); }), "more than 50 groups"), "more than 50 groups were accepted"); + std::string padded = TropConf; + std::string padding = "\"padding\":["; + for (std::size_t value = 0; value < 1048576; ++value) padding += "0,"; + padded.insert(1, padding + "0],"); + const auto oversized = ucp({{"tropconf.json", padded}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&oversized] { NpTrophy2::ParseTrophyPackage(oversized, "en-US"); }), "tropconf.json is not JSON: the document holds more than 1048576 values"), + "a tropconf.json of more than 1048576 values was read"); + check(contains(failure([] { NpTrophy2::ParseTrophyPackage("not a package", "en-US"); }), "fewer than the 64-byte header"), "a package that is not a container was read"); + check(NpTrophy2::LanguageTag(1) == "en-US" && NpTrophy2::LanguageTag(0) == "ja-JP" && NpTrophy2::LanguageTag(2) == "fr-FR" && NpTrophy2::LanguageTag(29) == "id-ID", "language tags are wrong"); + bool thrown = false; + try { + NpTrophy2::LanguageTag(31); + } catch (const std::out_of_range&) { + thrown = true; + } + check(thrown, "language 31 has a tag"); +} + +void testProgressAndPlatinum() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + std::set unlocked; + const NpTrophy2::UnlockedQuery isUnlocked = [&unlocked](std::int32_t id) { return unlocked.contains(id); }; + const auto& progressive = package.trophies.at(5); + check(NpTrophy2::TrophyProgress(progressive, std::nullopt, false) == 0 && NpTrophy2::TrophyProgress(progressive, 3, false) == 3 && NpTrophy2::TrophyProgress(progressive, 9, false) == 5 && + NpTrophy2::TrophyProgress(progressive, 1, true) == 5, + "progress is not the stat up to the target"); + bool thrown = false; + try { + NpTrophy2::TrophyProgress(package.trophies.at(1), 3, false); + } catch (const std::invalid_argument&) { + thrown = true; + } + check(thrown, "a trophy that is not progressive reported progress"); + + unlocked = {1, 2, 3, 4}; + check(!NpTrophy2::PlatinumToUnlock(package, 4, isUnlocked), "the platinum unlocked before trophy 5"); + unlocked.insert(5); + check(NpTrophy2::PlatinumToUnlock(package, 5, isUnlocked) == std::optional(0), "the platinum did not unlock after the last trophy that names it"); + check(!NpTrophy2::PlatinumToUnlock(package, 6, isUnlocked) && !NpTrophy2::PlatinumToUnlock(package, 0, isUnlocked) && !NpTrophy2::PlatinumToUnlock(package, 42, isUnlocked), + "a trophy that names no platinum unlocked one"); + unlocked.insert(0); + check(!NpTrophy2::PlatinumToUnlock(package, 5, isUnlocked), "an unlocked platinum was unlocked again"); + + unlocked = {1, 6}; + const auto all = NpTrophy2::CountTrophies(package, std::nullopt, isUnlocked); + check(all.trophies == 8 && all.unlocked == 2 && all.grades[NpTrophy2::GradePlatinum] == 1 && all.grades[NpTrophy2::GradeGold] == 1 && all.grades[NpTrophy2::GradeSilver] == 1 && + all.grades[NpTrophy2::GradeBronze] == 5 && all.unlockedGrades[NpTrophy2::GradeGold] == 1 && all.unlockedGrades[NpTrophy2::GradeBronze] == 1, + "the game counts are wrong"); + check(all.Percentage() == (6 + 1) * 100 / (6 + 2 + 5), "the progress percentage does not weigh gold 6, silver 2 and bronze 1"); + const auto expansion = NpTrophy2::CountTrophies(package, 1, isUnlocked); + check(expansion.trophies == 2 && expansion.unlocked == 1 && expansion.Percentage() == 50, "the group counts are wrong"); + check(NpTrophy2::CountTrophies(package, 5, isUnlocked).trophies == 0 && NpTrophy2::CountTrophies(package, 5, isUnlocked).Percentage() == 0, "an empty group has trophies"); +} + +void testStatRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + check(rules.stats.size() == 4 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && + rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0, + "stat definitions were not read"); + check(rules.events.size() == 3 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && + rules.events.at("STEPS")[0].input == "player.steps" && rules.events.at("TIME")[0].statId == 30, + "extraction rules were not read"); + std::set skipped(rules.skipped.begin(), rules.skipped.end()); + const auto reported = [&skipped](const char* part) { + for (const auto& line : skipped) { + if (contains(line, part)) return true; + } + return false; + }; + check(rules.skipped.size() == 9, "unsupported definitions and rules were not all reported"); + check(reported("stat 50 has aggregation \"sum\"") && reported("stat 60 has dataType \"int32\"") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId"), + "unsupported definitions were not reported"); + check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition"), + "unsupported rules were not reported"); + for (const char* input : {"$", "$.", "$..a", "$.a.", "$.a-b", "counter", "$.a b"}) { + const std::string extraction = std::string(R"({"statsExtractionRuleArray":[{"condition":{"eventName":"E"},"action":{"input":")") + input + R"(","output":{"statId":10}}}]})"; + check(NpTrophy2::ParseStatRules(ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", extraction}})).events.empty(), "an unsupported input path was accepted"); + } + const auto withoutDefinitions = ucp({{"stats_extraction.json", StatsExtraction}}); + check(contains(failure([&withoutDefinitions] { NpTrophy2::ParseStatRules(withoutDefinitions); }), "no stats_definition.json"), "rules without definitions were read"); + const auto withoutRules = ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", "{}"}}); + check(contains(failure([&withoutRules] { NpTrophy2::ParseStatRules(withoutRules); }), "has no statsExtractionRuleArray"), "rules without an extraction array were read"); +} + +void testConditions() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + std::set unlocked; + const NpTrophy2::UnlockedQuery isUnlocked = [&unlocked](std::int32_t id) { return unlocked.contains(id); }; + const auto reached = [&](std::uint64_t stat, std::uint64_t value) { return NpTrophy2::ReachedTrophies(package, {{stat, value}}, isUnlocked); }; + check(reached(10, 9).empty() && reached(10, 10) == std::vector{1} && reached(10, 11) == std::vector{1}, "ge did not unlock at its target"); + check(reached(20, 100).empty() && reached(20, 101) == std::vector{2}, "gt did not unlock past its target"); + check(reached(30, 6).empty() && reached(30, 5) == std::vector{3} && reached(30, 3) == std::vector{3} && reached(30, 2) == (std::vector{3, 4}), + "le and lt did not unlock at and below their targets"); + check(reached(40, 4).empty() && reached(40, 5) == std::vector{5}, "the progressive trophy did not unlock at its target"); + check(reached(99, 1000).empty() && NpTrophy2::ReachedTrophies(package, {}, isUnlocked).empty(), "a stat without a trophy, or no stat, unlocked a trophy"); + unlocked.insert(1); + check(reached(10, 10).empty(), "an unlocked trophy was reached again"); + check(NpTrophy2::ReachedTrophies(package, {{10, 10}, {20, 101}, {30, 0}, {40, 9}}, isUnlocked) == (std::vector{2, 3, 4, 5}), "one set of stats did not reach every trophy"); +} + +void testStatStore() { + const std::filesystem::path directory("stats"); + { + NpTrophy2::StatStore stats(directory); + check(!stats.Value(InitialUser, 0, 10) && stats.Values(InitialUser, 0).empty(), "a new store has stats"); + stats.Set(InitialUser, 0, 10, 7); + stats.Set(InitialUser, 0, 18446744073709551615ull, 1); + stats.Set(InitialUser, 0, 10, 9); + stats.Set(InitialUser + 1, 0, 10, 3); + stats.Set(InitialUser, 1, 10, 4); + check(stats.RecordPath(InitialUser, 0) == directory / "268435456_0.stats.txt", "the stats are not stored next to the unlocks"); + } + NpTrophy2::StatStore restarted(directory); + check(restarted.Value(InitialUser, 0, 10) == std::optional(9) && restarted.Value(InitialUser + 1, 0, 10) == std::optional(3) && + restarted.Value(InitialUser, 1, 10) == std::optional(4) && restarted.Values(InitialUser, 0).size() == 2, + "stats did not survive a restart"); + for (const char* text : {"10\n", "10 x\n", "x 5\n", "-1 5\n", "10 5\n10 6\n", "10 5 7\n", "\n", "10 -5\n"}) { + std::filesystem::remove_all("damaged"); + std::filesystem::create_directories("damaged"); + std::FILE* file = std::fopen("damaged/268435456_0.stats.txt", "wb"); + check(file != nullptr, "a damaged stat record was not created"); + std::fputs(text, file); + std::fclose(file); + NpTrophy2::StatStore damaged("damaged"); + check(contains(failure([&damaged] { damaged.Values(InitialUser, 0); }), "stat record"), "a damaged stat record was read"); + } +} + +NpTrophy2Event event(const char* name, std::vector properties) { + return {name, std::move(properties)}; +} + +void testApplyStatRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + NpTrophy2::StatStore stats("applied"); + const auto none = NpTrophy2::ApplyStatRules(rules, event("UNKNOWN", {{"counter", 5, "5"}}), stats, InitialUser, 0); + check(none.updates.empty() && none.problems.empty() && stats.Values(InitialUser, 0).empty(), "an unknown event changed a stat"); + const auto collect = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 12, "12"}, {"total", 300, "300"}, {"other", 1, "1"}}), stats, InitialUser, 0); + check(collect.problems.empty() && collect.updates.size() == 2 && collect.updates[0].statId == 10 && collect.updates[0].value == 12 && collect.updates[0].input == "counter" && + collect.updates[1].statId == 20 && collect.updates[1].value == 300, + "an event did not update every stat it feeds"); + check(stats.Value(InitialUser, 0, 10) == std::optional(12) && stats.Value(InitialUser, 0, 20) == std::optional(300), "the updated stats were not stored"); + const auto missing = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 13, "13"}}), stats, InitialUser, 0); + check(missing.updates.size() == 1 && missing.problems.size() == 1 && missing.problems[0].rule == "COLLECT $.total for stat 20" && missing.problems[0].reason == "the event has no such property" && + stats.Value(InitialUser, 0, 20) == std::optional(300), + "a missing property changed its stat"); + const auto wrong = NpTrophy2::ApplyStatRules(rules, event("TIME", {{"seconds", std::nullopt, "-3"}}), stats, InitialUser, 0); + check(wrong.updates.empty() && wrong.problems.size() == 1 && wrong.problems[0].reason == "-3 is not an unsigned integer" && !stats.Value(InitialUser, 0, 30), "a negative value changed a stat"); + const auto wide = NpTrophy2::ApplyStatRules(rules, event("TIME", {{"seconds", 4294967296ull, "4294967296"}}), stats, InitialUser, 0); + check(wide.updates.empty() && wide.problems.size() == 1 && wide.problems[0].reason == "4294967296 is not from 0 to 4294967295", "a uint32 stat took a value it cannot hold"); + const auto bounded = NpTrophy2::ApplyStatRules(rules, event("STEPS", {{"player.steps", 51, "51"}}), stats, InitialUser, 0); + check(bounded.updates.empty() && bounded.problems[0].reason == "51 is not from 0 to 50", "a value above maxValue changed a stat"); + const auto nested = NpTrophy2::ApplyStatRules(rules, event("STEPS", {{"steps", 2, "2"}, {"player.steps", 50, "50"}}), stats, InitialUser, 0); + check(nested.updates.size() == 1 && nested.updates[0].statId == 40 && stats.Value(InitialUser, 0, 40) == std::optional(50), "a nested property was not read"); + const auto latest = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 2, "2"}, {"total", 1, "1"}}), stats, InitialUser, 0); + check(latest.updates.size() == 2 && stats.Value(InitialUser, 0, 10) == std::optional(2), "a stat did not keep the latest value"); +} + +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-package-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + testContainer(); + testJson(); + testPackage(); + testProgressAndPlatinum(); + testStatRules(); + testConditions(); + testStatStore(); + testApplyStatRules(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy package tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp new file mode 100644 index 0000000000..10352c9edf --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp @@ -0,0 +1,304 @@ +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" { +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2DestroyContext(int); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*); +} + +namespace { + +constexpr std::int32_t InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +bool throws(TAction action) { + try { + action(); + } catch (const std::exception&) { + return true; + } + return false; +} + +std::string read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +void write(const char* path, const std::string& text) { + std::FILE* file = std::fopen(path, "wb"); + check(file != nullptr, "test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + check(std::fclose(file) == 0 && written, "test file was not written"); +} + +void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +std::string ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + putBigEndian(data, 0, 0xB228C60A, 4); + putBigEndian(data, 4, 1, 4); + putBigEndian(data, 8, size, 8); + putBigEndian(data, 0x10, files.size(), 4); + putBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + putBigEndian(data, entry + 0x20, offset, 8); + putBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + check(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + check(saved >= 0 && _dup2(_fileno(file), _fileno(stderr)) == 0, "cannot capture stderr"); +#else + saved = dup(fileno(stderr)); + check(saved >= 0 && dup2(fileno(file), fileno(stderr)) == fileno(stderr), "cannot capture stderr"); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +void testPersistence() { + const std::filesystem::path directory("store"); + { + NpTrophy2::TrophyStore store(directory); + check(!store.UnlockTick(InitialUser, 0, 12) && store.UnlockCount(InitialUser, 0) == 0, "a new store has unlocks"); + check(store.Unlock(InitialUser, 0, 12, 1000), "trophy 12 was not unlocked"); + check(!store.Unlock(InitialUser, 0, 12, 2000), "trophy 12 was unlocked twice"); + check(store.Unlock(InitialUser, 0, 3, 3000), "trophy 3 was not unlocked"); + check(store.Unlock(InitialUser + 1, 0, 12, 4000) && store.Unlock(InitialUser, 1, 12, 5000), "another user or service label shares unlocks"); + check(throws([&store] { store.Unlock(InitialUser, 0, -1, 6000); }), "a negative trophy id was unlocked"); + check(store.RecordPath(InitialUser, 0) == directory / "268435456_0.txt", "the record is not named after the user and service label"); + } + check(read("store/268435456_0.txt") == "3 3000\n12 1000\n", "the record does not list the unlocks"); + NpTrophy2::TrophyStore restarted(directory); + check(restarted.UnlockTick(InitialUser, 0, 12) == std::optional(1000) && restarted.UnlockTick(InitialUser, 0, 3) == std::optional(3000), + "unlock times did not survive a restart"); + check(restarted.UnlockCount(InitialUser, 0) == 2 && restarted.UnlockCount(InitialUser + 1, 0) == 1 && restarted.UnlockCount(InitialUser, 1) == 1 && restarted.UnlockCount(InitialUser, 2) == 0, + "unlock counts did not survive a restart"); + check(!restarted.Unlock(InitialUser, 0, 12, 7000) && restarted.UnlockTick(InitialUser, 0, 12) == std::optional(1000), "a restart forgot an unlock"); + check(restarted.Unlock(InitialUser, 0, 7, 8000) && read("store/268435456_0.txt") == "3 3000\n7 8000\n12 1000\n", "an unlock after a restart lost earlier unlocks"); +} + +void testDamagedRecords() { + const std::filesystem::path directory("damaged"); + for (const char* text : {"12\n", "12 x\n", "x 5\n", "-1 5\n", "12 5\n12 6\n", "12 5 7\n", "\n", "12 5\r\n"}) { + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + write("damaged/268435456_0.txt", text); + NpTrophy2::TrophyStore store(directory); + check(throws([&store] { store.UnlockCount(InitialUser, 0); }), "a damaged record was read"); + } +} + +struct Seen { + std::vector unlocks; +}; + +void remember(void* context, const NpTrophy2Unlock& unlock) { + static_cast(context)->unlocks.push_back(unlock); +} + +NpTrophy2Data trophyData(int context, int trophyId) { + NpTrophy2Data data{}; + check(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, nullptr, &data) == 0, "GetTrophyInfo failed"); + check(data.trophy_id == trophyId, "GetTrophyInfo answered for another trophy"); + return data; +} + +std::uint32_t unlockedTrophies(int context) { + NpTrophy2GameData data{}; + check(sceNpTrophy2GetGameInfo(context, 1, nullptr, &data) == 0, "GetGameInfo failed"); + return data.unlocked_trophies; +} + +struct SlowListener { + std::atomic running{false}; + std::atomic finished{false}; +}; + +void slowly(void* context, const NpTrophy2Unlock&) { + auto* state = static_cast(context); + state->running = true; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + state->finished = true; +} + +void testListenerRemovalWaitsForItsCallback() { + SlowListener state; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(slowly, &state); + std::thread unlocking([] { NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 2, 1); }); + while (!state.running) std::this_thread::yield(); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + const bool finished = state.finished; + unlocking.join(); + check(finished, "a listener was removed while its callback was still running, so its context could be freed under it"); +} + +void testQueries() { + int context = 0; + check(sceNpTrophy2CreateContext(&context, InitialUser, 0, 0) == 0, "context was not created"); + int other = 0; + check(sceNpTrophy2CreateContext(&other, InitialUser + 1, 0, 0) == 0 && other != context, "second context was not created"); + const auto locked = trophyData(context, 12); + check(!locked.unlocked && locked.timestamp_tick == 0, "trophy 12 is unlocked before its event"); + check(unlockedTrophies(context) == 0, "the game has unlocks before any event"); + + Seen seen; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(remember, &seen); + const auto before = NpTrophy2::CurrentTick(); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was not recorded"); + check(!NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was recorded twice"); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 0), "trophy 0 was not recorded"); + const auto after = NpTrophy2::CurrentTick(); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 5) && seen.unlocks.size() == 2, "the listener did not see each new unlock once"); + const auto& first = seen.unlocks.front(); + check(first.titleId == "PPSA00000" && first.userId == InitialUser && first.serviceLabel == 0 && first.trophyId == 12 && first.tick >= before && first.tick <= after, + "the listener did not get the unlock's title, user, trophy and time"); + check(before > NpTrophy2::UnixEpochTick, "the current tick is not counted from year 1"); + + const auto unlocked = trophyData(context, 12); + check(unlocked.unlocked && unlocked.timestamp_tick == first.tick, "trophy 12 is not reported unlocked at its time"); + check(!trophyData(context, 13).unlocked, "trophy 13 is reported unlocked"); + check(unlockedTrophies(context) == 3, "the game does not count its unlocks"); + NpTrophy2Data listed{}; + std::uint32_t count = 0; + check(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 1, nullptr, &listed, &count) == 0 && count == 1 && listed.trophy_id == 0 && listed.unlocked, + "the trophy list does not report trophy 0 unlocked"); + check(!trophyData(other, 12).unlocked && unlockedTrophies(other) == 0, "another user sees the unlocks"); + check(read("_trophy/268435456_0.txt").find("12 ") != std::string::npos, "the unlock was not stored in the title folder"); + check(throws([] { NpTrophy2AddUnlockListener_nid_no_patch(nullptr, nullptr); }), "a null listener was added"); + check(throws([listener] { NpTrophy2RemoveUnlockListener_nid_no_patch(listener); }), "a removed listener was removed again"); + check(sceNpTrophy2DestroyContext(context) == 0 && sceNpTrophy2DestroyContext(other) == 0, "contexts were not destroyed"); +} + + +void testPostedUnlockRules() { + std::filesystem::create_directories("app0/sce_sys/trophy2"); + std::filesystem::create_directories("app0/sce_sys/uds"); + write("app0/sce_sys/trophy2/trophy04.ucp", ucp({{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0"}, + {"id":"2","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"3","grade":"B"}]})"}, + {"tropmeta_en-US.json", R"({"metadata":{"titleMetadata":{"name":"Rules"},"trophyMetadata":[]}})"}})); + write("app0/sce_sys/uds/uds04.ucp", ucp({{"stats_definition.json", R"({"statDefinitionArray":[{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"}, + {"stats_extraction.json", R"({"statsExtractionRuleArray":[{"condition":{"eventName":"STEPS"},"action":{"input":"$.steps","output":{"statId":40}}}]})"}})); + check(NpTrophy2::Package(4) != nullptr, "the trophy data of service label 4 was not loaded"); + Seen seen; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(remember, &seen); + StderrCapture capture; + const bool unknown = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 9); + const bool platinum = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 0); + const bool progressive = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 2); + const auto log = capture.Finish(); + check(!unknown && !platinum && !progressive, "an unknown, platinum or progressive trophy posted through UDS was recorded"); + check(NpTrophy2::TitleStore().UnlockCount(InitialUser, 4) == 0 && seen.unlocks.empty(), "a refused unlock reached the record or a listener"); + for (const char* line : {"[trophy] ignored the posted unlock of trophy 9 for user 268435456: the title's trophy data has no such trophy\n", + "[trophy] ignored the posted unlock of trophy 0 for user 268435456: it is the platinum, which unlocks with the last trophy that names it\n", + "[trophy] ignored the posted unlock of trophy 2 for user 268435456: it is progressive and unlocks through its statistic\n"}) { + check(log.find(line) != std::string::npos, "a refused unlock did not log its trophy and reason"); + } + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 1) && NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 3), "an ordinary trophy posted through UDS was not recorded"); + check(!NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 0), "the platinum unlocked before every trophy that names it"); + NpTrophy2ApplyEvent_nid_no_patch(InitialUser, 4, NpTrophy2Event{"STEPS", {{"steps", 5, "5"}}}); + check(NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 2).has_value(), "the progressive trophy did not unlock through its statistic"); + check(NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 0).has_value(), "the platinum did not unlock with the last trophy that names it"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + std::vector seenIds; + for (const auto& unlock : seen.unlocks) seenIds.push_back(unlock.trophyId); + check(seenIds == std::vector{1, 3, 2, 0}, "the listener did not see the posted, statistic and platinum unlocks once, in order"); +} + +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-store-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::filesystem::current_path(root); + write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + testPersistence(); + testDamagedRecords(); + testQueries(); + testPostedUnlockRules(); + testListenerRemovalWaitsForItsCallback(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy store tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index ffa5b0803b..d9163d1dc7 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -5,11 +5,62 @@ add_library(libSceNpUniversalDataSystem SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/EventPropertyObject.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/EventPropertyArray.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UdsEvent.cpp ) target_include_directories(libSceNpUniversalDataSystem PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src ) -target_link_libraries(libSceNpUniversalDataSystem PRIVATE libc) +target_link_libraries(libSceNpUniversalDataSystem PRIVATE libSceNpTrophy2 libc) configure_windows_unwind(libSceNpUniversalDataSystem) + +add_test_executable(np_uds_events_tests EXCLUDE_FROM_ALL + tests/Events.cpp + src/Context.cpp + src/Event.cpp + src/EventPropertyArray.cpp + src/EventPropertyObject.cpp + src/UdsEvent.cpp +) +target_include_directories(np_uds_events_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(np_uds_events_tests PRIVATE libSceNpTrophy2 libc) +configure_windows_unwind(np_uds_events_tests) +if(BUILD_TESTING) + set_target_properties(np_uds_events_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME np_uds_events COMMAND np_uds_events_tests) + if(WIN32) + set_tests_properties(np_uds_events PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_trophy_unlocks_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyUnlocks.cpp) + target_include_directories(guest_np_trophy_unlocks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_unlocks_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) + configure_windows_unwind(guest_np_trophy_unlocks_tests) + add_test(NAME guest_np_trophy_unlocks COMMAND guest_np_trophy_unlocks_tests) + if(WIN32) + set_tests_properties(guest_np_trophy_unlocks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_trophy_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyData.cpp) + target_include_directories(guest_np_trophy_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_data_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) + configure_windows_unwind(guest_np_trophy_data_tests) + add_test(NAME guest_np_trophy_data COMMAND guest_np_trophy_data_tests) + if(WIN32) + set_tests_properties(guest_np_trophy_data PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_trophy_stats_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyStats.cpp) + target_include_directories(guest_np_trophy_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_stats_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) + configure_windows_unwind(guest_np_trophy_stats_tests) + add_test(NAME guest_np_trophy_stats COMMAND guest_np_trophy_stats_tests) + if(WIN32) + set_tests_properties(guest_np_trophy_stats PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp b/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp new file mode 100644 index 0000000000..35c8196824 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp @@ -0,0 +1,68 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPUNIVERSALDATASYSTEM_INCLUDE_UDSEVENT_HPP +#define CORE_LIBS_PRX_LIBSCENPUNIVERSALDATASYSTEM_INCLUDE_UDSEVENT_HPP + +#include +#include +#include +#include +#include +#include +#include + +struct NpUniversalDataSystemEventPropertyObject; +struct NpUniversalDataSystemEventPropertyArray; +struct NpTrophy2Event; + +namespace Uds { + +using Binary = std::vector; +using Value = std::variant, std::shared_ptr>; + +Value Clone(const Value& value); + +} + +struct NpUniversalDataSystemEventPropertyObject { + std::vector> properties; + + Uds::Value& Set(std::string_view key, Uds::Value value); + const Uds::Value* Find(std::string_view key) const; + NpUniversalDataSystemEventPropertyObject Clone() const; +}; + +struct NpUniversalDataSystemEventPropertyArray { + std::vector values; + + Uds::Value& Add(Uds::Value value); + NpUniversalDataSystemEventPropertyArray Clone() const; +}; + +struct NpUniversalDataSystemEvent { + std::string name; + NpUniversalDataSystemEventPropertyObject properties; +}; + +namespace Uds { + +inline constexpr std::string_view UnlockTrophyEvent = "_UnlockTrophy"; +inline constexpr std::string_view UpdateTrophyProgressEvent = "_UpdateTrophyProgress"; +inline constexpr std::string_view TrophyIdProperty = "_trophy_id"; + +enum class TrophyEventKind { None, Unlock, Progress, Malformed }; + +struct TrophyEvent { + TrophyEventKind kind = TrophyEventKind::None; + std::int32_t trophyId = -1; + std::string problem; +}; + +TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event); +std::string Describe(const Value& value); +std::string Describe(const NpUniversalDataSystemEventPropertyObject& object); +std::string Describe(const NpUniversalDataSystemEvent& event); +NpTrophy2Event StatEvent(const NpUniversalDataSystemEvent& event); + +} + +#endif diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp index da087ae06f..f73c4a2cb2 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp @@ -1,21 +1,58 @@ #include +#include +#include +#include #include +#include #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace Uds { + +namespace { + +struct Contexts { + std::mutex mutex; + std::map owners; + int next = NP_UNIVERSAL_DATA_SYSTEM_CONTEXT_DEFAULT; +}; + +Contexts& contexts() { + static Contexts table; + return table; +} + +} + +std::optional FindContext(int context) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + const auto found = table.owners.find(context); + if (found == table.owners.end()) return std::nullopt; + return found->second; +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) { if (context == nullptr) { throw std::invalid_argument(std::string(__func__) + ": invalid argument context"); } - *context = NP_UNIVERSAL_DATA_SYSTEM_CONTEXT_DEFAULT; + auto& table = Uds::contexts(); + std::lock_guard lock(table.mutex); + *context = table.next++; + table.owners[*context] = {user_id, service_label}; return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyContext(int context) { + auto& table = Uds::contexts(); + std::lock_guard lock(table.mutex); + table.owners.erase(context); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp index ae5ea0f668..be7df0ae46 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp @@ -1,13 +1,60 @@ #include #include +#include +#include #include +#include +#include +#include #include #include #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" +namespace { + +Uds::Owned& events() { + static Uds::Owned registry; + return registry; +} + +bool firstProgress(std::int32_t trophyId) { + static std::mutex mutex; + static std::set logged; + std::lock_guard lock(mutex); + return logged.insert(trophyId).second; +} + +void ignore(const NpUniversalDataSystemEvent& event, int context, const std::string& reason) { + std::fprintf(stderr, "[trophy] ignored %s posted to context %d: %s\n", Uds::Describe(event).c_str(), context, reason.c_str()); +} + +void post(const NpUniversalDataSystemEvent& event, int context) { + static const bool trace = std::getenv("APS5_TRACE_UDS") != nullptr; + if (trace) std::fprintf(stderr, "[uds] context %d posted %s\n", context, Uds::Describe(event).c_str()); + const auto trophy = Uds::RecognizeTrophyEvent(event); + switch (trophy.kind) { + case Uds::TrophyEventKind::None: + if (const auto owner = Uds::FindContext(context)) NpTrophy2ApplyEvent_nid_no_patch(owner->userId, owner->serviceLabel, Uds::StatEvent(event)); + return; + case Uds::TrophyEventKind::Malformed: + ignore(event, context, trophy.problem); + return; + case Uds::TrophyEventKind::Progress: + if (firstProgress(trophy.trophyId)) ignore(event, context, "trophy progress is not recorded"); + return; + case Uds::TrophyEventKind::Unlock: + if (const auto owner = Uds::FindContext(context)) NpTrophy2RecordUnlock_nid_no_patch(owner->userId, owner->serviceLabel, trophy.trophyId); + else ignore(event, context, "the context does not exist"); + return; + } +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateEvent( @@ -19,21 +66,30 @@ int APS5_VABI sceNpUniversalDataSystemCreateEvent( if (event_name == nullptr || new_event == nullptr) { APS5_INVALID_ARG_EX; } - *new_event = new NpUniversalDataSystemEvent; + auto event = std::make_unique(); + event->name = event_name; + if (prop != nullptr) event->properties = prop->Clone(); + *new_event = events().Adopt(std::move(event)); if (prop_ptr != nullptr) { - *prop_ptr = (prop != nullptr - ? const_cast(prop) - : new NpUniversalDataSystemEventPropertyObject); + *prop_ptr = &(*new_event)->properties; } return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent* event) { - delete event; + events().Release(event); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemPostEvent(int context, int handle, const void* event, uint64_t options) { + if (event == nullptr) { + APS5_INVALID_ARG_EX; + } + const auto* posted = static_cast(event); + if (!events().Contains(posted)) { + throw std::invalid_argument(std::string(__func__) + ": the event was not created by sceNpUniversalDataSystemCreateEvent or was destroyed"); + } + post(*posted, context); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp index fd1c988b32..57e985ba8f 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp @@ -1,21 +1,33 @@ #include #include +#include +#include +#include #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +Uds::Owned& arrays() { + static Uds::Owned registry; + return registry; +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyArray(NpUniversalDataSystemEventPropertyArray** new_array) { if (new_array == nullptr) { APS5_INVALID_ARG_EX; } - *new_array = new NpUniversalDataSystemEventPropertyArray; + *new_array = arrays().Adopt(std::make_unique()); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyArray(NpUniversalDataSystemEventPropertyArray* array) { - delete array; + arrays().Release(array); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -25,6 +37,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetString( if (array == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(std::string(value)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -34,6 +47,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt32( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -43,6 +57,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt32( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -52,6 +67,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt64( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -61,6 +77,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt64( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -70,6 +87,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetFloat32( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -79,6 +97,7 @@ NpUniversalDataSystemEventPropertyArray* array, double value) if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -88,6 +107,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBool( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -97,6 +117,8 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBinary( if (array == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + const auto* bytes = static_cast(value); + array->Add(Uds::Binary(bytes, bytes + value_size)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -108,11 +130,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetObject( if (array == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyObject{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast(value) - : new NpUniversalDataSystemEventPropertyObject); + *value_ptr = child.get(); } + array->Add(std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -124,11 +146,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetArray( if (array == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyArray{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast(value) - : new NpUniversalDataSystemEventPropertyArray); + *value_ptr = child.get(); } + array->Add(std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp index 48fc34544c..d316c1ea30 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp @@ -1,21 +1,33 @@ #include #include +#include +#include +#include #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +Uds::Owned& objects() { + static Uds::Owned registry; + return registry; +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject** new_object) { if (new_object == nullptr) { APS5_INVALID_ARG_EX; } - *new_object = new NpUniversalDataSystemEventPropertyObject; + *new_object = objects().Adopt(std::make_unique()); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject* object) { - delete object; + objects().Release(object); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -25,6 +37,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetString( if (object == nullptr || key == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, std::string(value)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -34,6 +47,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -43,6 +57,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -52,6 +67,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -61,6 +77,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -70,6 +87,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -79,6 +97,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat64( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -88,6 +107,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBool( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -97,6 +117,8 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBinary( if (object == nullptr || key == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + const auto* bytes = static_cast(value); + object->Set(key, Uds::Binary(bytes, bytes + value_size)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -109,11 +131,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyObject{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast(value) - : new NpUniversalDataSystemEventPropertyObject); + *value_ptr = child.get(); } + object->Set(key, std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -126,11 +148,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetArray( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyArray{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast(value) - : new NpUniversalDataSystemEventPropertyArray); + *value_ptr = child.get(); } + object->Set(key, std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp b/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp index 4a02cb68e0..e89083d5bd 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp @@ -3,8 +3,14 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" +#include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" static constexpr int SCE_NP_UNIVERSAL_DATA_SYSTEM_OK = 0; static constexpr int SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT = -2141900542; @@ -15,4 +21,44 @@ static constexpr int NP_UNIVERSAL_DATA_SYSTEM_HANDLE_DEFAULT = 1; static constexpr const char* NP_UNIVERSAL_DATA_SYSTEM_EMPTY_JSON = "{}"; static constexpr size_t NP_UNIVERSAL_DATA_SYSTEM_EMPTY_EVENT_SIZE = 3; +namespace Uds { + +template +class Owned { +public: + TValue* Adopt(std::unique_ptr value) { + std::lock_guard lock(mutex); + auto* pointer = value.get(); + values.emplace(pointer, std::move(value)); + return pointer; + } + + void Release(const TValue* value) { + std::unique_ptr released; + std::lock_guard lock(mutex); + const auto found = values.find(value); + if (found == values.end()) return; + released = std::move(found->second); + values.erase(found); + } + + bool Contains(const TValue* value) { + std::lock_guard lock(mutex); + return values.contains(value); + } + +private: + std::mutex mutex; + std::map> values; +}; + +struct ContextOwner { + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; +}; + +std::optional FindContext(int context); + +} + #endif diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp new file mode 100644 index 0000000000..201178ddbf --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp @@ -0,0 +1,187 @@ +#include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include + +namespace Uds { + +Value Clone(const Value& value) { + if (const auto* object = std::get_if>(&value)) return std::make_shared((*object)->Clone()); + if (const auto* array = std::get_if>(&value)) return std::make_shared((*array)->Clone()); + return value; +} + +namespace { + +void appendQuoted(std::string& out, std::string_view text) { + out += '"'; + for (const char character : text) { + if (character == '"' || character == '\\') { + out += '\\'; + out += character; + } else if (static_cast(character) < 0x20) { + char escaped[8]; + std::snprintf(escaped, sizeof(escaped), "\\u%04x", static_cast(static_cast(character))); + out += escaped; + } else { + out += character; + } + } + out += '"'; +} + +void appendValue(std::string& out, const Value& value); + +void appendObject(std::string& out, const NpUniversalDataSystemEventPropertyObject& object) { + out += '{'; + bool first = true; + for (const auto& [key, value] : object.properties) { + if (!first) out += ','; + first = false; + appendQuoted(out, key); + out += ':'; + appendValue(out, value); + } + out += '}'; +} + +void appendValue(std::string& out, const Value& value) { + std::visit([&](const auto& held) { + using Held = std::decay_t; + char number[40]; + if constexpr (std::is_same_v) { + out += held ? "true" : "false"; + } else if constexpr (std::is_same_v || std::is_same_v) { + std::snprintf(number, sizeof(number), "%lld", static_cast(held)); + out += number; + } else if constexpr (std::is_same_v || std::is_same_v) { + std::snprintf(number, sizeof(number), "%llu", static_cast(held)); + out += number; + } else if constexpr (std::is_same_v) { + std::snprintf(number, sizeof(number), "%.9g", static_cast(held)); + out += number; + } else if constexpr (std::is_same_v) { + std::snprintf(number, sizeof(number), "%.17g", held); + out += number; + } else if constexpr (std::is_same_v) { + appendQuoted(out, held); + } else if constexpr (std::is_same_v) { + std::snprintf(number, sizeof(number), "<%zu bytes>", held.size()); + out += number; + } else if constexpr (std::is_same_v>) { + appendObject(out, *held); + } else { + out += '['; + for (std::size_t i = 0; i < held->values.size(); ++i) { + if (i != 0) out += ','; + appendValue(out, held->values[i]); + } + out += ']'; + } + }, value); +} + +std::optional unsignedValue(const Value& value) { + return std::visit([](const auto& held) -> std::optional { + using Held = std::decay_t; + if constexpr (std::is_same_v || std::is_same_v) { + return held; + } else if constexpr (std::is_same_v || std::is_same_v) { + if (held < 0) return std::nullopt; + return static_cast(held); + } else { + return std::nullopt; + } + }, value); +} + +void flatten(const NpUniversalDataSystemEventPropertyObject& object, const std::string& prefix, std::vector& properties) { + for (const auto& [key, value] : object.properties) { + auto path = prefix.empty() ? key : prefix + "." + key; + if (const auto* child = std::get_if>(&value)) { + flatten(**child, path, properties); + continue; + } + properties.push_back({std::move(path), unsignedValue(value), Describe(value)}); + } +} + +} + +TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event) { + TrophyEvent result; + if (event.name == UnlockTrophyEvent) result.kind = TrophyEventKind::Unlock; + else if (event.name == UpdateTrophyProgressEvent) result.kind = TrophyEventKind::Progress; + else return result; + const auto* id = event.properties.Find(TrophyIdProperty); + const auto* value = id != nullptr ? std::get_if(id) : nullptr; + if (value != nullptr && *value >= 0) { + result.trophyId = *value; + } else if (result.kind == TrophyEventKind::Unlock) { + result.kind = TrophyEventKind::Malformed; + result.problem = id == nullptr ? "it has no _trophy_id" : value == nullptr ? "its _trophy_id is not an int32" : "its _trophy_id is negative"; + } + return result; +} + +std::string Describe(const Value& value) { + std::string out; + appendValue(out, value); + return out; +} + +std::string Describe(const NpUniversalDataSystemEventPropertyObject& object) { + std::string out; + appendObject(out, object); + return out; +} + +std::string Describe(const NpUniversalDataSystemEvent& event) { + return event.name + " " + Describe(event.properties); +} + +NpTrophy2Event StatEvent(const NpUniversalDataSystemEvent& event) { + NpTrophy2Event converted; + converted.name = event.name; + flatten(event.properties, {}, converted.properties); + return converted; +} + +} + +Uds::Value& NpUniversalDataSystemEventPropertyObject::Set(std::string_view key, Uds::Value value) { + for (auto& [name, stored] : properties) { + if (name == key) { + stored = std::move(value); + return stored; + } + } + return properties.emplace_back(std::string(key), std::move(value)).second; +} + +const Uds::Value* NpUniversalDataSystemEventPropertyObject::Find(std::string_view key) const { + for (const auto& [name, stored] : properties) { + if (name == key) return &stored; + } + return nullptr; +} + +NpUniversalDataSystemEventPropertyObject NpUniversalDataSystemEventPropertyObject::Clone() const { + NpUniversalDataSystemEventPropertyObject copy; + copy.properties.reserve(properties.size()); + for (const auto& [name, stored] : properties) copy.properties.emplace_back(name, Uds::Clone(stored)); + return copy; +} + +Uds::Value& NpUniversalDataSystemEventPropertyArray::Add(Uds::Value value) { + return values.emplace_back(std::move(value)); +} + +NpUniversalDataSystemEventPropertyArray NpUniversalDataSystemEventPropertyArray::Clone() const { + NpUniversalDataSystemEventPropertyArray copy; + copy.values.reserve(values.size()); + for (const auto& stored : values) copy.values.push_back(Uds::Clone(stored)); + return copy; +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp new file mode 100644 index 0000000000..d9cca747e4 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp @@ -0,0 +1,296 @@ +#include "NpUniversalDataSystem.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemDestroyContext(int); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetString(NpUniversalDataSystemEventPropertyObject*, const char*, const char*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32(NpUniversalDataSystemEventPropertyObject*, const char*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64(NpUniversalDataSystemEventPropertyObject*, const char*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64(NpUniversalDataSystemEventPropertyObject*, const char*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32(NpUniversalDataSystemEventPropertyObject*, const char*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat64(NpUniversalDataSystemEventPropertyObject*, const char*, double); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBool(NpUniversalDataSystemEventPropertyObject*, const char*, bool); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBinary(NpUniversalDataSystemEventPropertyObject*, const char*, const void*, size_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetArray(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyArray*, NpUniversalDataSystemEventPropertyArray**); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyArray(NpUniversalDataSystemEventPropertyArray**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyArray(NpUniversalDataSystemEventPropertyArray*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetString(NpUniversalDataSystemEventPropertyArray*, const char*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt32(NpUniversalDataSystemEventPropertyArray*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt32(NpUniversalDataSystemEventPropertyArray*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt64(NpUniversalDataSystemEventPropertyArray*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt64(NpUniversalDataSystemEventPropertyArray*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetFloat32(NpUniversalDataSystemEventPropertyArray*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetFloat64(NpUniversalDataSystemEventPropertyArray*, double); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBool(NpUniversalDataSystemEventPropertyArray*, bool); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBinary(NpUniversalDataSystemEventPropertyArray*, const void*, size_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetObject(NpUniversalDataSystemEventPropertyArray*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetArray(NpUniversalDataSystemEventPropertyArray*, const NpUniversalDataSystemEventPropertyArray*, NpUniversalDataSystemEventPropertyArray**); +} + +namespace { + +constexpr int InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void call(int result, const char* message) { + check(result == 0, message); +} + +template +bool throwsInvalidArgument(TAction action) { + try { + action(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +NpUniversalDataSystemEventPropertyObject* createObject() { + NpUniversalDataSystemEventPropertyObject* object = nullptr; + call(sceNpUniversalDataSystemCreateEventPropertyObject(&object), "property object was not created"); + check(object != nullptr, "property object is null"); + return object; +} + +void testObjectSetters() { + auto* object = createObject(); + const std::uint8_t bytes[] = {1, 2, 3}; + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(object, "int32", -5), "SetInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(object, "uint32", 4000000000u), "SetUInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt64(object, "int64", -9000000000), "SetInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(object, "uint64", 18000000000000000000ull), "SetUInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetFloat32(object, "float32", 0.5f), "SetFloat32 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetFloat64(object, "float64", 0.25), "SetFloat64 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetBool(object, "bool", true), "SetBool failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetString(object, "string", "a\"b\\c\n"), "SetString failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetBinary(object, "binary", bytes, sizeof(bytes)), "SetBinary failed"); + check(Uds::Describe(*object) == R"({"int32":-5,"uint32":4000000000,"int64":-9000000000,"uint64":18000000000000000000,"float32":0.5,"float64":0.25,"bool":true,"string":"a\"b\\c\u000a","binary":<3 bytes>})", + "properties were not kept with their types"); + const auto* binary = std::get_if(object->Find("binary")); + check(binary != nullptr && *binary == Uds::Binary{1, 2, 3}, "binary bytes were not copied"); + call(sceNpUniversalDataSystemEventPropertyObjectSetString(object, "int32", "replaced"), "SetString over int32 failed"); + const auto* replaced = std::get_if(object->Find("int32")); + check(object->properties.size() == 9 && replaced != nullptr && *replaced == "replaced", "setting a key again did not replace its value"); + check(object->Find("missing") == nullptr, "a missing key was found"); + check(throwsInvalidArgument([] { sceNpUniversalDataSystemEventPropertyObjectSetInt32(nullptr, "key", 1); }), "a null object was accepted"); + check(throwsInvalidArgument([object] { sceNpUniversalDataSystemEventPropertyObjectSetInt32(object, nullptr, 1); }), "a null key was accepted"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(object), "property object was not destroyed"); +} + +void testNestedValues() { + auto* source = createObject(); + auto* parent = createObject(); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(source, "x", 1), "SetInt32 on the source failed"); + NpUniversalDataSystemEventPropertyObject* child = nullptr; + call(sceNpUniversalDataSystemEventPropertyObjectSetObject(parent, "child", source, &child), "SetObject failed"); + check(child != nullptr && child != source, "SetObject did not return its own copy"); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(source, "x", 2), "SetInt32 after SetObject failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetBool(child, "y", true), "SetBool on the child failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(source), "source object was not destroyed"); + NpUniversalDataSystemEventPropertyArray* list = nullptr; + call(sceNpUniversalDataSystemEventPropertyObjectSetArray(parent, "list", nullptr, &list), "SetArray failed"); + check(list != nullptr, "SetArray did not return the stored array"); + const std::uint8_t bytes[] = {4, 5}; + call(sceNpUniversalDataSystemEventPropertyArraySetInt32(list, 1), "array SetInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetUInt32(list, 2), "array SetUInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetInt64(list, -3), "array SetInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetUInt64(list, 4), "array SetUInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetFloat32(list, 1.5f), "array SetFloat32 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetFloat64(list, 2.5), "array SetFloat64 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBool(list, true), "array SetBool failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetString(list, "s"), "array SetString failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBinary(list, bytes, sizeof(bytes)), "array SetBinary failed"); + NpUniversalDataSystemEventPropertyObject* element = nullptr; + call(sceNpUniversalDataSystemEventPropertyArraySetObject(list, nullptr, &element), "array SetObject failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(element, "z", 3), "SetUInt64 on the array element failed"); + NpUniversalDataSystemEventPropertyArray* inner = nullptr; + call(sceNpUniversalDataSystemEventPropertyArraySetArray(list, nullptr, &inner), "array SetArray failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBool(inner, false), "SetBool on the inner array failed"); + const char* expected = R"({"child":{"x":1,"y":true},"list":[1,2,-3,4,1.5,2.5,true,"s",<2 bytes>,{"z":3},[false]]})"; + check(Uds::Describe(*parent) == expected, "nested properties were not kept"); + const auto copy = parent->Clone(); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(child, "x", 5), "SetInt32 on the child failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBool(inner, true), "SetBool on the inner array failed"); + check(Uds::Describe(copy) == expected, "a clone shares nested values with its source"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(child), "destroying a child failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyArray(list), "destroying a child array failed"); + check(Uds::Describe(*parent) == R"({"child":{"x":5,"y":true},"list":[1,2,-3,4,1.5,2.5,true,"s",<2 bytes>,{"z":3},[false,true]]})", + "destroying a child took it from its parent"); + NpUniversalDataSystemEventPropertyArray* array = nullptr; + call(sceNpUniversalDataSystemCreateEventPropertyArray(&array), "property array was not created"); + call(sceNpUniversalDataSystemEventPropertyArraySetInt32(array, 9), "SetInt32 on the created array failed"); + NpUniversalDataSystemEventPropertyArray* stored = nullptr; + call(sceNpUniversalDataSystemEventPropertyObjectSetArray(parent, "list", array, &stored), "SetArray over a key failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyArray(array), "property array was not destroyed"); + check(stored != array && Uds::Describe(*parent) == R"({"child":{"x":5,"y":true},"list":[9]})", "SetArray did not store a copy under its key"); + check(throwsInvalidArgument([] { sceNpUniversalDataSystemEventPropertyArraySetInt32(nullptr, 1); }), "a null array was accepted"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(parent), "parent object was not destroyed"); +} + +void testEvents() { + auto* properties = createObject(); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", 12), "SetInt32 failed"); + NpUniversalDataSystemEvent* event = nullptr; + NpUniversalDataSystemEventPropertyObject* eventProperties = nullptr; + call(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", properties, &event, &eventProperties), "event was not created"); + check(event != nullptr && eventProperties == &event->properties, "CreateEvent did not return the event's property object"); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", 13), "SetInt32 after CreateEvent failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(properties), "property object was not destroyed"); + check(Uds::Describe(*event) == R"(_UnlockTrophy {"_trophy_id":12})", "the event did not copy its properties"); + call(sceNpUniversalDataSystemEventPropertyObjectSetString(eventProperties, "detail", "boss"), "SetString on the event failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(eventProperties), "destroying the event's property object failed"); + check(Uds::Describe(*event) == R"(_UnlockTrophy {"_trophy_id":12,"detail":"boss"})", "properties set through the returned pointer were lost"); + call(sceNpUniversalDataSystemDestroyEvent(event), "event was not destroyed"); + NpUniversalDataSystemEvent* empty = nullptr; + call(sceNpUniversalDataSystemCreateEvent("_Empty", nullptr, &empty, nullptr), "event without properties was not created"); + check(Uds::Describe(*empty) == "_Empty {}", "an event without properties was not empty"); + call(sceNpUniversalDataSystemDestroyEvent(empty), "event without properties was not destroyed"); + check(throwsInvalidArgument([] { + NpUniversalDataSystemEvent* unnamed = nullptr; + sceNpUniversalDataSystemCreateEvent(nullptr, nullptr, &unnamed, nullptr); + }), "an event without a name was created"); +} + +Uds::TrophyEvent recognize(const char* name, std::optional id) { + NpUniversalDataSystemEvent event; + event.name = name; + if (id) event.properties.Set(Uds::TrophyIdProperty, *id); + return Uds::RecognizeTrophyEvent(event); +} + +void testRecognition() { + const auto unlock = recognize("_UnlockTrophy", std::int32_t{12}); + check(unlock.kind == Uds::TrophyEventKind::Unlock && unlock.trophyId == 12, "an unlock event was not recognised"); + const auto first = recognize("_UnlockTrophy", std::int32_t{0}); + check(first.kind == Uds::TrophyEventKind::Unlock && first.trophyId == 0, "an unlock of trophy 0 was not recognised"); + const auto progress = recognize("_UpdateTrophyProgress", std::int32_t{3}); + check(progress.kind == Uds::TrophyEventKind::Progress && progress.trophyId == 3, "a progress event was not recognised"); + const auto anonymous = recognize("_UpdateTrophyProgress", std::nullopt); + check(anonymous.kind == Uds::TrophyEventKind::Progress && anonymous.trophyId == -1, "a progress event without a trophy id was not recognised"); + const auto missing = recognize("_UnlockTrophy", std::nullopt); + check(missing.kind == Uds::TrophyEventKind::Malformed && missing.problem == "it has no _trophy_id", "an unlock without a trophy id was accepted"); + const auto widened = recognize("_UnlockTrophy", std::uint32_t{12}); + check(widened.kind == Uds::TrophyEventKind::Malformed && widened.problem == "its _trophy_id is not an int32", "an unlock with an unsigned trophy id was accepted"); + const auto negative = recognize("_UnlockTrophy", std::int32_t{-1}); + check(negative.kind == Uds::TrophyEventKind::Malformed && negative.problem == "its _trophy_id is negative", "an unlock with a negative trophy id was accepted"); + for (const char* name : {"_unlockTrophy", "UnlockTrophy", "_UnlockTrophy2", "_UnlockTrophy ", "_StartActivity", ""}) { + check(recognize(name, std::int32_t{12}).kind == Uds::TrophyEventKind::None, "an unknown event was taken for a trophy event"); + } +} + +void testStatEvent() { + NpUniversalDataSystemEvent event; + event.name = "STEPS"; + event.properties.Set("count", std::uint32_t{4}); + event.properties.Set("signed", std::int32_t{-2}); + event.properties.Set("wide", std::int64_t{7}); + event.properties.Set("huge", std::uint64_t{18000000000000000000ull}); + event.properties.Set("ratio", 0.5); + event.properties.Set("name", std::string("x")); + auto player = std::make_shared(); + player->Set("steps", std::uint32_t{5}); + auto inner = std::make_shared(); + inner->Set("depth", std::int32_t{1}); + player->Set("inner", inner); + event.properties.Set("player", player); + auto list = std::make_shared(); + list->Add(std::int32_t{1}); + event.properties.Set("list", list); + const auto converted = Uds::StatEvent(event); + const std::vector, std::string>> expected{ + {"count", 4, "4"}, {"signed", std::nullopt, "-2"}, {"wide", 7, "7"}, {"huge", 18000000000000000000ull, "18000000000000000000"}, {"ratio", std::nullopt, "0.5"}, + {"name", std::nullopt, "\"x\""}, {"player.steps", 5, "5"}, {"player.inner.depth", 1, "1"}, {"list", std::nullopt, "[1]"}}; + check(converted.name == "STEPS" && converted.properties.size() == expected.size(), "the stat event does not hold every leaf property"); + for (std::size_t i = 0; i < expected.size(); ++i) { + const auto& [path, value, text] = expected[i]; + const auto& property = converted.properties[i]; + check(property.path == path && property.value == value && property.text == text, "a stat event property has the wrong path, value or text"); + } +} + +struct Unlocks { + int count = 0; +}; + +void countUnlock(void* context, const NpTrophy2Unlock&) { + ++static_cast(context)->count; +} + +void post(int context, const char* name, std::optional id) { + NpUniversalDataSystemEvent* event = nullptr; + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + call(sceNpUniversalDataSystemCreateEvent(name, nullptr, &event, &properties), "event was not created"); + if (id) properties->Set(Uds::TrophyIdProperty, *id); + call(sceNpUniversalDataSystemPostEvent(context, 1, event, 0), "event was not posted"); + call(sceNpUniversalDataSystemDestroyEvent(event), "event was not destroyed"); +} + +void testPostingIgnoredEvents() { + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(countUnlock, &unlocks); + int context = 0; + call(sceNpUniversalDataSystemCreateContext(&context, InitialUser, 0, 0), "context was not created"); + int second = 0; + call(sceNpUniversalDataSystemCreateContext(&second, InitialUser, 0, 0), "second context was not created"); + check(second != context, "two contexts share an id"); + post(context, "_StartActivity", std::int32_t{1}); + post(context, "_UnlockTrophy", std::string("1")); + post(context, "_UnlockTrophy", std::nullopt); + post(context, "_UpdateTrophyProgress", std::int32_t{2}); + post(context, "_UpdateTrophyProgress", std::int32_t{2}); + post(second + 1, "_UnlockTrophy", std::int32_t{4}); + call(sceNpUniversalDataSystemDestroyContext(context), "context was not destroyed"); + post(context, "_UnlockTrophy", std::int32_t{4}); + call(sceNpUniversalDataSystemDestroyContext(second), "second context was not destroyed"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + check(unlocks.count == 0, "an event that is not a valid unlock reached the trophy store"); + check(throwsInvalidArgument([] { sceNpUniversalDataSystemPostEvent(1, 1, nullptr, 0); }), "a null event was posted"); + NpUniversalDataSystemEvent stray; + stray.name = "_UnlockTrophy"; + check(throwsInvalidArgument([&stray] { sceNpUniversalDataSystemPostEvent(1, 1, &stray, 0); }), "an event that CreateEvent did not create was posted"); + NpUniversalDataSystemEvent* destroyed = nullptr; + call(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", nullptr, &destroyed, nullptr), "event was not created"); + call(sceNpUniversalDataSystemDestroyEvent(destroyed), "event was not destroyed"); + check(throwsInvalidArgument([destroyed] { sceNpUniversalDataSystemPostEvent(1, 1, destroyed, 0); }), "a destroyed event was posted"); +} + +} + +int main() { + try { + testObjectSetters(); + testNestedValues(); + testEvents(); + testRecognition(); + testStatEvent(); + testPostingIgnoredEvents(); + std::puts("UDS event tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} 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/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt new file mode 100644 index 0000000000..02e53fdc16 --- /dev/null +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libScePlayerSelectionDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libScePlayerSelectionDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libScePlayerSelectionDialog) + +if(BUILD_TESTING) + add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) + target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog) + add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) + if(WIN32) + set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index e32913a7ad..d866a19fe2 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -1,11 +1,70 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { +int APS5_VABI scePlayerSelectionDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI scePlayerSelectionDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + int APS5_VABI scePlayerSelectionDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp index 315ce70e71..d1c11f4133 100644 --- a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -59,4 +59,12 @@ int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { return 0; } +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChat(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePsml_debug/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/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 5635289707..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); 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/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index 8142808e06..99095ec97a 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -46,8 +46,9 @@ function(configure_ssl_backend) endfunction() configure_ssl_backend() -set_target_properties(mbedcrypto mbedx509 p256m PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(mbedtls mbedcrypto mbedx509 p256m PROPERTIES POSITION_INDEPENDENT_CODE ON) if(MINGW) + target_compile_options(mbedtls PRIVATE -fno-asynchronous-unwind-tables) target_compile_options(mbedcrypto PRIVATE -fno-asynchronous-unwind-tables) target_compile_options(mbedx509 PRIVATE -fno-asynchronous-unwind-tables) target_compile_options(p256m PRIVATE -fno-asynchronous-unwind-tables) @@ -55,7 +56,7 @@ endif() add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceSsl PRIVATE mbedx509 mbedcrypto) +target_link_libraries(libSceSsl PRIVATE mbedtls mbedx509 mbedcrypto) set_target_properties(libSceSsl PROPERTIES VISIBILITY_INLINES_HIDDEN ON) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceSsl PRIVATE LINKER:--exclude-libs,ALL) @@ -76,8 +77,17 @@ if(BUILD_TESTING) target_link_libraries(guest_ssl_load_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_load_tests) add_test(NAME guest_ssl_load COMMAND guest_ssl_load_tests) + add_test_executable(guest_ssl_transport_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslTransport.cpp) + target_include_directories(guest_ssl_transport_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_transport_tests PRIVATE libSceSsl libSceNet libkernel libc mbedtls mbedx509 mbedcrypto) + configure_windows_unwind(guest_ssl_transport_tests) + foreach(table IN ITEMS kernel net) + add_test(NAME guest_ssl_transport_${table} COMMAND guest_ssl_transport_tests ${table}) + set_tests_properties(guest_ssl_transport_${table} PROPERTIES TIMEOUT 30) + endforeach() if(WIN32) - set_tests_properties(guest_ssl guest_ssl_load PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + target_link_libraries(guest_ssl_transport_tests PRIVATE bcrypt ws2_32) + set_tests_properties(guest_ssl guest_ssl_load guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index b12d6bb913..f860d62aa1 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,15 +1,29 @@ +#ifdef _WIN32 +#include +#else +#include +#include +#endif + #include "SslTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" #include #include #include +#include +#include +#include +#include #include #include #include +#include #include #include +#include #include #include #include @@ -26,10 +40,31 @@ namespace { constexpr int ErrorNotFound = static_cast(0x8095f004); constexpr int ErrorInvalidArg = static_cast(0x8095177a); constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ReadPartial = 0x1; + +constexpr std::int64_t TicksPerSecond = 1000000; +constexpr std::int64_t TicksPerMinute = 60 * TicksPerSecond; +constexpr std::int64_t TicksPerHour = 60 * TicksPerMinute; +constexpr std::int64_t TicksPerDay = 24 * TicksPerHour; +constexpr std::int64_t UnixEpochDays = 719162; + +std::int64_t DaysFromCivil(std::int64_t year, unsigned month, unsigned day) { + year -= month <= 2; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const auto yearOfEra = static_cast(year - era * 400); + const unsigned dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const unsigned dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + static_cast(dayOfEra) - 719468 + UnixEpochDays; +} + +std::uint64_t TimeToTick(const mbedtls_x509_time& time) { + return static_cast(DaysFromCivil(time.year, time.mon, time.day)) * TicksPerDay + + time.hour * TicksPerHour + time.min * TicksPerMinute + time.sec * TicksPerSecond; +} struct Pool { std::size_t size, current = 0, peak = 0; - bool exhausted = false; + std::mutex mutex; }; struct alignas(std::max_align_t) Allocation { @@ -38,21 +73,23 @@ struct alignas(std::max_align_t) Allocation { }; thread_local Pool* currentPool = nullptr; +thread_local bool currentPoolExhausted = false; void* PoolCalloc(std::size_t count, std::size_t size) noexcept { if (!currentPool) return nullptr; if (size && count > std::numeric_limits::max() / size) { - currentPool->exhausted = true; + currentPoolExhausted = true; return nullptr; } const auto bytes = count * size; + std::lock_guard lock(currentPool->mutex); if (bytes > currentPool->size - currentPool->current || bytes > std::numeric_limits::max() - sizeof(Allocation)) { - currentPool->exhausted = true; + currentPoolExhausted = true; return nullptr; } auto* allocation = static_cast(std::calloc(1, sizeof(Allocation) + bytes)); if (!allocation) { - currentPool->exhausted = true; + currentPoolExhausted = true; return nullptr; } allocation->pool = currentPool; @@ -65,15 +102,23 @@ void* PoolCalloc(std::size_t count, std::size_t size) noexcept { void PoolFree(void* pointer) noexcept { if (!pointer) return; auto* allocation = static_cast(pointer) - 1; - allocation->pool->current -= allocation->size; + { + std::lock_guard lock(allocation->pool->mutex); + allocation->pool->current -= allocation->size; + } mbedtls_platform_zeroize(pointer, allocation->size); std::free(allocation); } struct PoolScope { Pool* previous; - explicit PoolScope(Pool& pool) : previous(std::exchange(currentPool, &pool)) { pool.exhausted = false; } - ~PoolScope() { currentPool = previous; } + bool previousExhausted; + explicit PoolScope(Pool& pool) + : previous(std::exchange(currentPool, &pool)), previousExhausted(std::exchange(currentPoolExhausted, false)) {} + ~PoolScope() { + currentPool = previous; + currentPoolExhausted = previousExhausted; + } }; struct PoolDeleter { @@ -95,7 +140,7 @@ std::size_t Add(std::size_t left, std::size_t right) { void CheckBackend(int error, const char* message) { if (error == 0) return; - if (currentPool && currentPool->exhausted) throw std::bad_alloc(); + if (currentPool && currentPoolExhausted) throw std::bad_alloc(); throw std::invalid_argument(message); } @@ -146,6 +191,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,22 +207,149 @@ struct Context { std::vector> certificateHandles; std::unordered_map caLists; std::unordered_map caCerts; - explicit Context(std::size_t size) : pool{size} {} + std::unordered_map> certNames; + explicit Context(std::size_t size) : pool{size, 0, 0, {}} {} +}; + +int HostError() { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +bool HostWouldBlock(int error) { +#ifdef _WIN32 + return error == WSAEWOULDBLOCK; +#else + return error == EAGAIN || error == EWOULDBLOCK; +#endif +} + +bool HostInterrupted(int error) { +#ifdef _WIN32 + return error == WSAEINTR; +#else + return error == EINTR; +#endif +} + +struct Session { + std::shared_ptr context; + std::shared_ptr identity; + std::shared_ptr socketOwner; + std::uintptr_t socket; + std::mutex mutex; + int hostError = 0; + Random random; + mbedtls_x509_crt authorities; + mbedtls_ssl_config config; + mbedtls_ssl_context ssl; + + Session(std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) + : context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { + mbedtls_x509_crt_init(&authorities); + mbedtls_ssl_config_init(&config); + mbedtls_ssl_init(&ssl); + } + + ~Session() { + PoolScope scope(context->pool); + mbedtls_ssl_free(&ssl); + mbedtls_ssl_config_free(&config); + mbedtls_x509_crt_free(&authorities); + } + + void Configure(const std::string& host) { + random.Seed(); + for (const auto& chain : context->authorities) + for (const auto* entry = &chain->root; entry; entry = entry->next) + CheckBackend(mbedtls_x509_crt_parse_der(&authorities, entry->raw.p, entry->raw.len), "SSL CA certificate copy failed"); + CheckBackend(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT), + "SSL client configuration failed"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authorities, nullptr); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &random.drbg); + if (identity) + CheckBackend(mbedtls_ssl_conf_own_cert(&config, &identity->chain->root, &identity->key.key), "SSL client certificate setup failed"); + CheckBackend(mbedtls_ssl_setup(&ssl, &config), "SSL connection setup failed"); + CheckBackend(mbedtls_ssl_set_hostname(&ssl, host.c_str()), "SSL host name setup failed"); + mbedtls_ssl_set_bio(&ssl, this, Send, Receive, nullptr); + } + + static int Send(void* argument, const unsigned char* buffer, std::size_t length) { + auto& session = *static_cast(argument); + const auto chunk = static_cast(std::min(length, INT_MAX)); + for (;;) { +#ifdef _WIN32 + const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); +#else + const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); +#endif + if (sent >= 0) return static_cast(sent); + const int error = HostError(); + if (HostInterrupted(error)) continue; + if (HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; + session.hostError = error; + return MBEDTLS_ERR_NET_SEND_FAILED; + } + } + + static int Receive(void* argument, unsigned char* buffer, std::size_t length) { + auto& session = *static_cast(argument); + const auto chunk = static_cast(std::min(length, INT_MAX)); + for (;;) { +#ifdef _WIN32 + const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); +#else + const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); +#endif + if (received >= 0) return static_cast(received); + const int error = HostError(); + if (HostInterrupted(error)) continue; + if (HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_READ; + session.hostError = error; + return MBEDTLS_ERR_NET_RECV_FAILED; + } + } + + [[noreturn]] void Fail(const char* function, int error) const { + if (currentPoolExhausted || error == MBEDTLS_ERR_SSL_ALLOC_FAILED) throw std::bad_alloc(); + if (error == MBEDTLS_ERR_SSL_WANT_READ || error == MBEDTLS_ERR_SSL_WANT_WRITE) + throw std::runtime_error(std::string(function) + ": the socket would block or its timeout expired; the console's error code is unknown"); + char text[160]; + mbedtls_strerror(error, text, sizeof(text)); + auto message = std::string(function) + ": " + text; + if (hostError) message += ", host socket error " + std::to_string(hostError); + if (error == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { + char detail[512]; + if (mbedtls_x509_crt_verify_info(detail, sizeof(detail), "", mbedtls_ssl_get_verify_result(&ssl)) > 0) message += ", " + std::string(detail); + } + throw std::runtime_error(message + "; the console's error code is unknown"); + } }; 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; + std::shared_ptr session; }; struct Runtime { std::mutex mutex; - std::unordered_map> contexts; + std::unordered_map> contexts; std::unordered_map connections; unsigned nextHandle = 1; Runtime() { @@ -197,6 +378,13 @@ SslConnection& RequireConnection(const char* function, int sslConnectionId) { return connection->second; } +std::shared_ptr RequireSession(const char* function, int sslConnectionId) { + std::lock_guard lock(State().mutex); + auto& connection = RequireConnection(function, sslConnectionId); + if (!connection.session) throw std::logic_error(std::string(function) + ": the connection is not connected"); + return connection.session; +} + struct Input { Block bytes; std::size_t size; @@ -236,6 +424,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" { @@ -245,7 +476,7 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { std::lock_guard lock(state.mutex); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); const auto handle = static_cast(state.nextHandle); - state.contexts.emplace(handle, std::make_unique(poolSize)); + state.contexts.emplace(handle, std::make_shared(poolSize)); ++state.nextHandle; return handle; } @@ -266,7 +497,8 @@ int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* st std::memset(stats, 0, sizeof(*stats)); return static_cast(0x8095F006); } - const auto& pool = found->second->pool; + auto& pool = found->second->pool; + std::lock_guard poolLock(pool.mutex); *stats = {pool.size, pool.peak, pool.current, 0}; return 0; } @@ -415,14 +647,18 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) auto& state = State(); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL connection handles exhausted"); const auto id = static_cast(state.nextHandle); - state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr}); + state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr, {}}); ++state.nextHandle; return id; } int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { + std::shared_ptr session; std::lock_guard lock(State().mutex); - if (!State().connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + const auto found = State().connections.find(sslConnectionId); + if (found == State().connections.end()) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + session = std::move(found->second.session); + State().connections.erase(found); return 0; } @@ -435,76 +671,207 @@ int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbf } int APS5_VABI sceSslConnect(int sslConnectionId) { - (void)sslConnectionId; - NotImplemented_nid_no_patch(__func__); - return 0; + std::shared_ptr session; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& connection = RequireConnection(__func__, sslConnectionId); + if (connection.session) throw std::logic_error("sceSslConnect: the connection is already connected"); + if (connection.verifyCallback) throw std::invalid_argument("sceSslConnect: how the console invokes the verify callback is unknown"); + auto context = state.contexts.at(connection.context); + if (context->authorities.empty()) + throw std::runtime_error("sceSslConnect: no CA certificate is loaded, and the console's root certificates are not shipped"); + std::shared_ptr owner; + std::uintptr_t native = 0; + if (!KernelLookupSocketNative_nid_no_patch(connection.socket, &owner, &native)) + throw std::invalid_argument("sceSslConnect: descriptor " + std::to_string(connection.socket) + " is not a socket"); + PoolScope scope(context->pool); + session = std::make_shared(std::move(context), std::move(owner), native); + session->Configure(connection.host); + connection.session = session; + } + std::unique_lock sessionLock(session->mutex); + PoolScope scope(session->context->pool); + const int result = mbedtls_ssl_handshake(&session->ssl); + if (result == 0) return 0; + { + std::lock_guard lock(State().mutex); + const auto found = State().connections.find(sslConnectionId); + if (found != State().connections.end() && found->second.session == session) found->second.session.reset(); + } + session->Fail(__func__, result); } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { - (void)sslConnectionId; - (void)buf; - (void)len; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; + if (flags != 0 && flags != ReadPartial) throw std::invalid_argument("sceSslRead: unsupported flags " + std::to_string(flags)); + if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); + const auto session = RequireSession(__func__, sslConnectionId); + std::lock_guard sessionLock(session->mutex); + PoolScope scope(session->context->pool); + const int result = mbedtls_ssl_read(&session->ssl, static_cast(buf), std::min(len, INT_MAX)); + if (result >= 0) return result; + if (result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0; + session->Fail(__func__, result); } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { + if (flags != 0) throw std::invalid_argument("sceSslWrite: unsupported flags " + std::to_string(flags)); + if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); + const auto session = RequireSession(__func__, sslConnectionId); + std::lock_guard sessionLock(session->mutex); + PoolScope scope(session->context->pool); + const auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t written = 0; + while (written < total) { + const int result = mbedtls_ssl_write(&session->ssl, bytes + written, total - written); + if (result < 0) session->Fail(__func__, result); + written += static_cast(result); + } + return static_cast(written); +} + +int APS5_VABI sceSslClose(int sslConnectionId) { (void)sslConnectionId; - (void)buf; - (void)len; - (void)flags; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslClose() { - NotImplemented_nid_no_patch(__func__); - 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__); +void* APS5_VABI sceSslGetIssuerName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, false); +} + +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; } -int APS5_VABI sceSslGetNameEntryInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslGetNotBefore(int sslCtxId, void* sslCert, SceSsl::RtcTick* begin) { + if (!begin) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + begin->tick = TimeToTick(crt->valid_from); + return 0; +} + +int APS5_VABI sceSslGetNotAfter(int sslCtxId, void* sslCert, SceSsl::RtcTick* limit) { + if (!limit) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + limit->tick = TimeToTick(crt->valid_to); + return 0; +} + +int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, unsigned char* fingerprint) { + if (!fingerprint) return ErrorInvalidArg; + mbedtls_md_type_t mdType; + switch (hashType) { + case 0: mdType = MBEDTLS_MD_MD5; break; + case 1: mdType = MBEDTLS_MD_SHA1; break; + case 2: mdType = MBEDTLS_MD_SHA224; break; + case 3: mdType = MBEDTLS_MD_SHA256; break; + case 4: mdType = MBEDTLS_MD_SHA384; break; + case 5: mdType = MBEDTLS_MD_SHA512; break; + default: return ErrorInvalidArg; + } + const auto* info = mbedtls_md_info_from_type(mdType); + if (!info) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + if (mbedtls_md(info, crt->raw.p, crt->raw.len, fingerprint) != 0) return ErrorInvalidArg; return 0; } @@ -513,14 +880,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/libSceSsl/MbedConfig.hpp b/core/libs/prx/libSceSsl/MbedConfig.hpp index ab4361c8a7..aab4833dc9 100644 --- a/core/libs/prx/libSceSsl/MbedConfig.hpp +++ b/core/libs/prx/libSceSsl/MbedConfig.hpp @@ -2,3 +2,5 @@ #define MBEDTLS_PLATFORM_MEMORY #undef MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED +#undef MBEDTLS_SSL_PROTO_TLS1_3 +#undef MBEDTLS_SELF_TEST diff --git a/core/libs/prx/libSceSsl/SslTypes.hpp b/core/libs/prx/libSceSsl/SslTypes.hpp index 18fc70f740..f82ac92e0e 100644 --- a/core/libs/prx/libSceSsl/SslTypes.hpp +++ b/core/libs/prx/libSceSsl/SslTypes.hpp @@ -26,9 +26,14 @@ struct MemoryPoolStats { std::uint32_t reserved; }; +struct RtcTick { + std::uint64_t tick; +}; + static_assert(sizeof(Data) == 0x10); static_assert(sizeof(CaCerts) == 0x18); static_assert(sizeof(CaList) == 0x10); static_assert(sizeof(MemoryPoolStats) == 0x20); +static_assert(sizeof(RtcTick) == 0x8); } 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..0e66ec6bd9 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,10 +1,11 @@ #include +#include #include #include #include #include #include -#include +#include #include #include #include @@ -26,18 +27,32 @@ namespace { -int ConsoleLanguage() { - const char* configured = std::getenv("ANYPS5_LANGUAGE"); - if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; - const std::string_view text(configured); - int language = -1; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language); - if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { - throw std::runtime_error("sceSystemServiceParamGetInt: ANYPS5_LANGUAGE must be a console language number from " + - std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) + - ", not \"" + std::string(text) + "\""); - } - 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"; @@ -183,6 +198,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 +225,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); @@ -264,11 +283,11 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { } switch (paramId) { case SYSTEM_SERVICE_PARAM_ID_LANG: - case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break; + case SYSTEM_SERVICE_PARAM_ID_400: *value = SystemServiceConsoleLanguage("sceSystemServiceParamGetInt"); 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/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 99b73cc0b5..7b1664cc90 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -1,8 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP #define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP +#include #include #include +#include +#include +#include +#include +#include constexpr int SYSTEM_SERVICE_OK = 0; constexpr int SYSTEM_SERVICE_ERROR_PARAMETER = -2136932349; @@ -31,4 +37,18 @@ constexpr int SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR = 1; constexpr int SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF = 0; constexpr int SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS = 1; +inline int SystemServiceConsoleLanguage(std::string_view caller) { + const char* configured = std::getenv("ANYPS5_LANGUAGE"); + if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; + const std::string_view text(configured); + int language = -1; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language); + if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { + throw std::runtime_error(std::string(caller) + ": ANYPS5_LANGUAGE must be a console language number from " + + std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) + + ", not \"" + std::string(text) + "\""); + } + return language; +} + #endif diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index 449131a43a..6d606a67e8 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_user_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserService.cpp) target_include_directories(guest_user_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_user_service_tests PRIVATE libSceUserService) + configure_windows_unwind(guest_user_service_tests) add_test(NAME guest_user_service COMMAND guest_user_service_tests) if(WIN32) set_tests_properties(guest_user_service PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index a2c906db17..9f258f9aa8 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -26,6 +26,11 @@ int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32 return ReportAccessibilitySetting(user_id, chat_transcription, 0); } +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hold_delay) { return ReportAccessibilitySetting(user_id, press_and_hold_delay, 0); } @@ -38,6 +43,11 @@ int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibr return ReportAccessibilitySetting(user_id, vibration, 1); } +int APS5_VABI sceUserServiceGetAccessibilityZoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zoom_enabled) { return ReportAccessibilitySetting(user_id, zoom_enabled, 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 d4f3bc584a..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,27 @@ 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) @@ -70,6 +110,20 @@ if(BUILD_TESTING) 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) @@ -147,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) @@ -626,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}) @@ -725,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 @@ -758,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/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..34cee2743f 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -9,7 +9,7 @@ #include "general/VabiMacros.hpp" #include "general/ExportMacros.hpp" -extern "C" void NotImplemented_nid_no_patch(const char* funcName); +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName); extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle); extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); #ifndef _WIN32 @@ -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/GuestWriteWatch.hpp b/core/libs/prx/libc/include/GuestWriteWatch.hpp index 95d5df1fc5..9afcf582db 100644 --- a/core/libs/prx/libc/include/GuestWriteWatch.hpp +++ b/core/libs/prx/libc/include/GuestWriteWatch.hpp @@ -12,6 +12,7 @@ bool GuestWriteWatchAvailable_nid_postfix(); void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes); bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t bytes); bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes); +bool GuestWriteWatchOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes); bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context); void GuestWriteWatchBeginHostWrite_nid_postfix(const void* pointer, std::size_t bytes); void GuestWriteWatchEndHostWrite_nid_postfix(const void* pointer, std::size_t 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/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/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 16025ecbb9..fa8e028a98 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,12 +326,11 @@ 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) { @@ -344,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); @@ -545,6 +553,39 @@ void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t co return nullptr; } +int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare) { + if (size < 4) { + errno = 22; + return -1; + } + if (count == 0) return 0; + if (!compare) throw std::invalid_argument("mergesort: null comparator"); + if (!base) throw std::invalid_argument("mergesort: null base"); + if (count > (std::numeric_limits::max() - 8) / size) throw std::overflow_error("mergesort: array size overflow"); + auto* const scratch = static_cast(malloc_nid_postfix(count * size + 8)); + if (!scratch) return -1; + auto* source = static_cast(base); + auto* target = scratch; + for (size_t width = 1; width < count; width *= 2) { + for (size_t low = 0; low < count; low += 2 * width) { + const size_t middle = std::min(low + width, count); + const size_t high = std::min(low + 2 * width, count); + size_t left = low, right = middle, out = low; + while (left < middle && right < high) { + const size_t taken = compare(source + left * size, source + right * size) <= 0 ? left++ : right++; + std::memcpy(target + out++ * size, source + taken * size, size); + } + std::memcpy(target + out * size, source + left * size, (middle - left) * size); + out += middle - left; + std::memcpy(target + out * size, source + right * size, (high - right) * size); + } + std::swap(source, target); + } + if (source != base) std::memcpy(base, source, count * size); + free_nid_postfix(scratch); + return 0; +} + void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)) { if (!compare) throw std::invalid_argument("qsort: null comparator"); if (size == 0) throw std::invalid_argument("qsort: zero element size"); diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 85f2d9ed8d..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); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 2bbfbb1f5e..d96670de58 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; @@ -560,6 +614,6 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { } #endif -extern "C" void NotImplemented_nid_no_patch(const char* funcName) { +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) { throw std::runtime_error(std::string(funcName) + " not implemented"); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index d6bc09c0af..a931983d75 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); @@ -339,8 +385,13 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi bool progressed = false; if (state != nullptr && state->lock.owns_lock()) { state->lock.unlock(); - if (waiter != nullptr) progressed = waiter(address, bytes); - else std::this_thread::yield(); + try { + if (waiter != nullptr) progressed = waiter(address, bytes); + else std::this_thread::yield(); + } catch (...) { + state->lock.lock(); + throw; + } state->lock.lock(); } else { std::this_thread::yield(); diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index e155b3ed21..52907e1cb0 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -90,6 +90,13 @@ public: return covers(begin, end); } + bool Overlaps(std::uintptr_t begin, std::uintptr_t end) { + if (!Available()) return false; + std::shared_lock lock(_lock); + const auto next = _ranges.upper_bound(begin); + return (next != _ranges.begin() && std::prev(next)->second > begin) || (next != _ranges.end() && next->first < end); + } + bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) { if (!Available()) return false; begin &= ~(PageBytes - 1); @@ -379,6 +386,13 @@ public: return covers(begin, end); } + bool Overlaps(std::uintptr_t begin, std::uintptr_t end) { + if (!_available) return false; + const Lock lock(_lock); + const auto next = _ranges.upper_bound(begin); + return (next != _ranges.begin() && std::prev(next)->second.end > begin) || (next != _ranges.end() && next->first < end); + } + enum class Take { All, Armed, Fresh }; bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context, Take take = Take::All) { @@ -660,6 +674,16 @@ bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes #endif } +bool GuestWriteWatchOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes) { +#ifdef APS5_GUEST_WRITE_WATCH + return bytes != 0 && address + bytes > address && Watch::Get().Overlaps(address, address + bytes); +#else + static_cast(address); + static_cast(bytes); + return false; +#endif +} + bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) { #ifdef APS5_GUEST_WRITE_WATCH if (bytes == 0 || address + bytes < address) return false; 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 a464f6881f..cdc69a38b9 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -157,12 +157,8 @@ std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; -// The guest's wchar_t is 16 bits, so the wide facet needs its own entries: collate reaches the -// same slots with ranges of 16-bit characters, whose length is a count of characters and not of bytes. -// The C locale orders them by value, as Dinkumware's char_traits::compare does, mixes one -// whole character per hash round, and copies them into the result, whose size and capacity are bytes. constexpr std::size_t GuestWideCharacterSize = 2; -constexpr std::size_t GuestWideInlineCapacity = sizeof(GuestLocale::String::buffer) - GuestWideCharacterSize; +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"); @@ -195,15 +191,15 @@ GuestLocale::String* APS5_VABI WideCollateTransform(GuestLocale::String* result, const auto bytes = length * GuestWideCharacterSize; char* data = result->buffer; std::size_t capacity = GuestWideInlineCapacity; - if (bytes > GuestWideInlineCapacity) { - capacity = bytes | GuestStringCapacityMask; - data = static_cast(AllocateStringStorage(capacity + GuestWideCharacterSize)); + 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 = bytes; + result->size = length; result->capacity = capacity; return result; } @@ -333,6 +329,17 @@ GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_post return &g_classicLocale; } +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"); diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 7e91356869..ddeea98f8c 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -126,6 +126,23 @@ struct ExtendedParts { std::uint16_t signExponent; }; +template +TValue Hypot3(TValue x, TValue y, TValue z, THypot hypot) { + if (std::isnan(x)) return x; + if (std::isnan(y)) return y; + if (std::isnan(z)) return z; + if (std::isinf(x) || std::isinf(y) || std::isinf(z)) return std::numeric_limits::infinity(); + if (x == 0) return hypot(y, z); + if (y == 0) return hypot(x, z); + if (z == 0) return hypot(x, y); + x = std::fabs(x); + y = std::fabs(y); + z = std::fabs(z); + if (z >= x && z >= y) return hypot(z, hypot(x, y)); + if (y >= x && y >= z) return hypot(y, hypot(x, z)); + return hypot(x, hypot(y, z)); +} + ExtendedParts SplitExtended(long double value) { unsigned char bytes[sizeof(long double)]; std::memcpy(bytes, &value, sizeof(value)); @@ -165,6 +182,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" { @@ -177,6 +216,22 @@ std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t numerator, std::int64_t den return std::lldiv(numerator, denominator); } +std::lldiv_t APS5_VABI imaxdiv_nid_postfix(std::int64_t numerator, std::int64_t denominator) { + return std::lldiv(numerator, denominator); +} + +std::int64_t APS5_VABI labs_nid_postfix(std::int64_t x) { + return x < 0 ? static_cast(0u - static_cast(x)) : x; +} + +std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t x) { + return labs_nid_postfix(x); +} + +std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t x) { + return labs_nid_postfix(x); +} + float APS5_VABI fmodf_nid_postfix(float x, float y) { return std::fmod(x, y); } float APS5_VABI asinf_nid_postfix(float x) { const int saved_errno = errno; @@ -220,6 +275,8 @@ double APS5_VABI exp2_nid_postfix(double x) { double APS5_VABI ldexp_nid_postfix(double x, int exponent) { return std::ldexp(x, exponent); } double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } +long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return std::scalbn(x, exponent); } +float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } // Guest long is 64-bit, including on Windows where native long is 32-bit. @@ -256,7 +313,38 @@ 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 acoshl_nid_postfix(long double x) { return std::acosh(x); } +long double APS5_VABI asinhl_nid_postfix(long double x) { return std::asinh(x); } +long double APS5_VABI atanhl_nid_postfix(long double x) { return std::atanh(x); } +long double APS5_VABI atan2l_nid_postfix(long double y, long double x) { return std::atan2(y, x); } +long double APS5_VABI cbrtl_nid_postfix(long double x) { return std::cbrt(x); } +long double APS5_VABI erfl_nid_postfix(long double x) { return std::erf(x); } +long double APS5_VABI erfcl_nid_postfix(long double x) { return std::erfc(x); } +long double APS5_VABI expm1l_nid_postfix(long double x) { return std::expm1(x); } +long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); } +long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } +long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } +long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } +long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { + if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); + return std::hypot(x, y); +} +float APS5_VABI hypot3f_nid_postfix(float x, float y, float z) { return Hypot3(x, y, z, hypotf_nid_postfix); } +double APS5_VABI hypot3_nid_postfix(double x, double y, double z) { return Hypot3(x, y, z, hypot_nid_postfix); } +long double APS5_VABI hypot3l_nid_postfix(long double x, long double y, long double z) { return Hypot3(x, y, z, hypotl_nid_postfix); } +float APS5_VABI j0f_nid_postfix(float x) { return static_cast(::j0(x)); } +float APS5_VABI j1f_nid_postfix(float x) { return static_cast(::j1(x)); } +float APS5_VABI jnf_nid_postfix(int order, float x) { return static_cast(::jn(order, x)); } +float APS5_VABI y0f_nid_postfix(float x) { return static_cast(::y0(x)); } +float APS5_VABI y1f_nid_postfix(float x) { return static_cast(::y1(x)); } +float APS5_VABI ynf_nid_postfix(int order, float x) { return static_cast(::yn(order, x)); } long double APS5_VABI asinl_nid_postfix(long double x) { const ExtendedParts parts = SplitExtended(x); @@ -302,7 +390,12 @@ 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); } +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); diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index 143d50eba2..5292c9fe89 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -265,19 +267,15 @@ void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void* handle, std::size_t return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); } -void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t size, std::size_t alignment) { - if (alignment == 0 || (alignment & (alignment - 1))) { - Error(22); - return nullptr; - } - const auto effective = std::max(alignment, 16); +void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t alignment, std::size_t size) { + if (alignment >> 63) return nullptr; + const auto effective = std::bit_ceil(std::max(alignment, MinimumAlignment)); std::lock_guard lock(arenaMutex); auto* arena = Find(handle); if (!pointer) return Allocate(arena, size, effective); std::map::iterator chunk; if (!FindUsed(arena, pointer, chunk)) return nullptr; - if (!size) { Release(*arena, chunk); return nullptr; } - const auto needed = AlignUp(size, Granule); + const auto needed = AlignUp(std::max(size, 1), MinimumAlignment); if (needed < size) { Error(12); return nullptr; } const auto start = chunk->first; const auto capacity = chunk->second.end - start; @@ -311,4 +309,46 @@ std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void* poin Error(22); return 0; } + +void* APS5_VABI sceLibcPafMspaceCreate_nid_postfix(const char* name, void* base, std::size_t size, unsigned flags, const void* options) { + if (options) throw std::runtime_error("sceLibcPafMspaceCreate: creation options are not supported"); + return sceLibcMspaceCreate_nid_postfix(name, base, size, flags); +} + +int APS5_VABI sceLibcPafMspaceDestroy_nid_postfix(void* handle) { + return sceLibcMspaceDestroy_nid_postfix(handle); +} + +void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void* handle, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceMalloc_nid_postfix(handle, size); +} + +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void* handle, void* pointer, const void* owner) { + (void)owner; + sceLibcMspaceFree_nid_postfix(handle, pointer); +} + +void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void* handle, std::size_t count, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceCalloc_nid_postfix(handle, count, size); +} + +void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void* handle, void* pointer, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceRealloc_nid_postfix(handle, pointer, size); +} + +void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceReallocalign_nid_postfix(handle, pointer, alignment, size); +} + +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStats_nid_postfix(handle, stats); +} + +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStatsFast_nid_postfix(handle, stats); +} } diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index fee93d88d6..5865fe5933 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -5,6 +5,54 @@ #include #include #include +#include + +namespace { + +constexpr std::size_t PendingOffset = 0xa; +constexpr std::int8_t LowSurrogatePending = 0x10; + +void StorePending(unsigned char* state, std::int8_t pending) { + const auto stored = static_cast(pending); + std::memcpy(state + PendingOffset, &stored, sizeof(stored)); +} + +std::size_t EncodeUtf8(char* destination, std::uint64_t value) { + if (value < 0x80) { + destination[0] = static_cast(value); + return 1; + } + int extra = 5; + unsigned lead = 0xfc; + int shift = 30; + if (value < 0x800) { extra = 1; lead = 0xc0; shift = 6; } + else if (value < 0x10000) { extra = 2; lead = 0xe0; shift = 12; } + else if (value < 0x200000) { extra = 3; lead = 0xf0; shift = 18; } + else if (value < 0x4000000) { extra = 4; lead = 0xf8; shift = 24; } + destination[0] = static_cast((value >> shift) | lead); + for (int index = 1; index <= extra; ++index) + destination[index] = static_cast(((value >> (shift - 6 * index)) & 0x3f) | 0x80); + return static_cast(extra + 1); +} + +bool DecodeUtf8Byte(unsigned char byte, std::uint64_t& value, std::int8_t& pending) { + if (pending > 0) { + if ((byte & 0xc0) != 0x80) return false; + value = (value << 6) | (byte & 0x3f); + --pending; + return true; + } + if (byte < 0x80) value = byte; + else if ((byte & 0xe0) == 0xc0) { value = byte & 0x1f; pending = 1; } + else if ((byte & 0xf0) == 0xe0) { value = byte & 0x0f; pending = 2; } + else if ((byte & 0xf8) == 0xf0) { value = byte & 0x07; pending = 3; } + else if (byte >= 0xfc) { value = byte & 0x03; pending = 5; } + else if ((byte & 0xfc) == 0xf8) { value = byte & 0x03; pending = 4; } + else return false; + return true; +} + +} extern "C" { @@ -51,6 +99,118 @@ std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value return 1; } +int APS5_VABI mblen_nid_postfix(const char* source, std::size_t count) { + return mbtowc_nid_postfix(nullptr, source, count); +} + +int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value) { + if (!destination) return 0; + const auto result = wcrtomb_nid_postfix(destination, value, nullptr); + return result == static_cast(-1) ? -1 : static_cast(result); +} + +std::size_t APS5_VABI c32rtomb_nid_postfix(char* destination, std::uint32_t value, void*) { + char scratch[8]; + if (!destination) { + destination = scratch; + value = 0; + } + return EncodeUtf8(destination, value); +} + +std::size_t APS5_VABI c16rtomb_nid_postfix(char* destination, std::uint16_t unit, void* state) { + if (!state) throw std::invalid_argument("c16rtomb: null conversion state"); + char scratch[8]; + if (!destination) { + destination = scratch; + unit = 0; + } + auto* const bytes = static_cast(state); + std::uint64_t value = unit; + if (bytes[PendingOffset] != 0) { + if (static_cast(unit + 0x2000) < 0xfc00) return static_cast(-1); + std::uint64_t high = 0; + std::memcpy(&high, bytes, sizeof(high)); + value = high | (unit & 0x3ffu); + StorePending(bytes, 0); + } else if (static_cast(unit + 0x2400) >= 0xfc00) { + const std::uint64_t high = ((static_cast(unit) << 10) + 0x10000) & 0x1ffc00; + std::memcpy(bytes, &high, sizeof(high)); + StorePending(bytes, 1); + return 0; + } + return EncodeUtf8(destination, value); +} + +std::size_t APS5_VABI mbrtoc32_nid_postfix(std::uint32_t* destination, const char* source, std::size_t count, void* state) { + if (!state) throw std::invalid_argument("mbrtoc32: null conversion state"); + auto* const bytes = static_cast(state); + if (!source) { + destination = nullptr; + source = ""; + count = 1; + } + std::uint64_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + auto pending = static_cast(bytes[PendingOffset]); + for (std::size_t consumed = 0; consumed < count;) { + const auto byte = static_cast(source[consumed++]); + if (!DecodeUtf8Byte(byte, value, pending)) { + errno = 86; + return static_cast(-1); + } + if (pending == 0) { + if (destination) *destination = static_cast(value); + StorePending(bytes, 0); + return value == 0 ? 0 : consumed; + } + } + std::memcpy(bytes, &value, sizeof(value)); + StorePending(bytes, pending); + return static_cast(-2); +} + +std::size_t APS5_VABI mbrtoc16_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state) { + if (!state) throw std::invalid_argument("mbrtoc16: null conversion state"); + auto* const bytes = static_cast(state); + if (!source) { + destination = nullptr; + source = ""; + count = 1; + } + std::uint64_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + auto pending = static_cast(bytes[PendingOffset]); + if (pending == LowSurrogatePending) { + if (destination) *destination = static_cast((value & 0x3ff) | 0xdc00); + StorePending(bytes, 0); + return static_cast(-3); + } + for (std::size_t consumed = 0; consumed < count;) { + const auto byte = static_cast(source[consumed++]); + if (!DecodeUtf8Byte(byte, value, pending)) { + errno = 86; + return static_cast(-1); + } + if (pending != 0) continue; + if (value >= 0x110000) { + errno = 86; + return static_cast(-1); + } + if (value >= 0x10000) { + if (destination) *destination = static_cast(((value >> 10) + 0x3fc0) | 0xd800); + StorePending(bytes, LowSurrogatePending); + return consumed; + } + if (destination) *destination = static_cast(value); + StorePending(bytes, 0); + return value == 0 ? 0 : consumed; + } + std::memcpy(bytes, &value, sizeof(value)); + StorePending(bytes, pending); + return static_cast(-2); +} + std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state) { const auto* wide = *source; std::size_t converted = 0; diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 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/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 49b48e16ca..787fe00b2b 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -96,14 +96,17 @@ void SaveTypeWordLocked() { } std::uint16_t g_rand48Seed[3]{0x330e, 0xabcd, 0x1234}; +constexpr std::uint64_t DefaultRand48Multiplier = 0x5deece66dull; +constexpr std::uint16_t DefaultRand48Addend = 0xb; +std::uint64_t g_rand48Multiplier = DefaultRand48Multiplier; +std::uint16_t g_rand48Addend = DefaultRand48Addend; +std::uint16_t g_seed48Result[3]{}; std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { if (seed == nullptr) throw std::invalid_argument(std::string(function) + ": null seed"); - constexpr std::uint64_t Multiplier = 0x5deece66dull; - constexpr std::uint64_t Addend = 0xb; constexpr std::uint64_t Mask = (1ull << 48) - 1; std::uint64_t value = (static_cast(seed[2]) << 32) | (static_cast(seed[1]) << 16) | seed[0]; - value = (value * Multiplier + Addend) & Mask; + value = (value * g_rand48Multiplier + g_rand48Addend) & Mask; seed[0] = static_cast(value); seed[1] = static_cast(value >> 16); seed[2] = static_cast(value >> 32); @@ -146,6 +149,25 @@ void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { g_rand48Seed[0] = 0x330e; g_rand48Seed[1] = static_cast(static_cast(seedval)); g_rand48Seed[2] = static_cast(static_cast(seedval) >> 16); + g_rand48Multiplier = DefaultRand48Multiplier; + g_rand48Addend = DefaultRand48Addend; +} + +std::uint16_t* APS5_VABI seed48_nid_postfix(const std::uint16_t* seed) { + if (seed == nullptr) throw std::invalid_argument("seed48: null seed"); + std::lock_guard lock(g_randomLock); + for (int index = 0; index < 3; ++index) g_rand48Seed[index] = g_seed48Result[index] = seed[index]; + g_rand48Multiplier = DefaultRand48Multiplier; + g_rand48Addend = DefaultRand48Addend; + return g_seed48Result; +} + +void APS5_VABI lcong48_nid_postfix(const std::uint16_t* parameters) { + if (parameters == nullptr) throw std::invalid_argument("lcong48: null parameters"); + std::lock_guard lock(g_randomLock); + for (int index = 0; index < 3; ++index) g_rand48Seed[index] = parameters[index]; + g_rand48Multiplier = parameters[3] | (static_cast(parameters[4]) << 16) | (static_cast(parameters[5]) << 32); + g_rand48Addend = parameters[6]; } std::int64_t APS5_VABI random_nid_postfix() { 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 8d136d05b4..9d59cff429 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include +#include #ifdef _WIN32 #include #else @@ -134,6 +137,16 @@ int APS5_VABI fileno_nid_postfix(FileStream* stream) { stream->GuestState().descriptor = static_cast(descriptor); return descriptor; } +void APS5_VABI clearerr_unlocked_nid_postfix(FileStream* stream) { + stream->ClearError(); +} +int APS5_VABI fileno_unlocked_nid_postfix(FileStream* stream) { + if (stream == nullptr || !stream->IsOpen()) return -1; + return fileno_nid_postfix(stream); +} +[[noreturn]] char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*) { + throw std::runtime_error("fgetln: the console's export is a hlt instruction, which ends the process"); +} 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; 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/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 1a4215cf35..225d81c4a7 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -152,6 +152,14 @@ char* APS5_VABI strrchr_nid_postfix(const char* s, int c) { return std::strrchr(const_cast(s), c); } +char* APS5_VABI rindex_nid_postfix(const char* s, int c) { + return strrchr_nid_postfix(s, c); +} + +int APS5_VABI flsl_nid_postfix(std::int64_t mask) { + return mask == 0 ? 0 : 64 - std::countl_zero(static_cast(mask)); +} + char* APS5_VABI strstr_nid_postfix(const char* haystack, const char* needle) { return std::strstr(const_cast(haystack), needle); } @@ -200,6 +208,10 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr return std::strtoull(str, endptr, base); } +unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base) { + return strtoull_nid_postfix(str, endptr, base); +} + double APS5_VABI strtod_nid_postfix(const char* str, char** endptr) { return ParseFloat(str, endptr, std::strtod); } @@ -478,6 +490,12 @@ size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept) { return count; } +size_t APS5_VABI wcscspn_nid_postfix(const char16_t* s, const char16_t* reject) { + size_t count = 0; + while (s[count] != 0 && !Contains(reject, s[count])) ++count; + return count; +} + char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last) { if (text == nullptr) text = *last; if (text == nullptr) return nullptr; @@ -532,6 +550,14 @@ unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t* return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); } +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoll_nid_postfix(str, endptr, base); +} + +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoull_nid_postfix(str, endptr, base); +} + std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t* str, char16_t** endptr, int base) { if (base < 0 || base == 1 || base > 36) throw std::invalid_argument("_WStoul: unsupported base " + std::to_string(base)); return wcstoul_nid_postfix(str, endptr, base); 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/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 21cf02d628..57c8c9537f 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,6 +381,7 @@ 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:$") @@ -382,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:$") @@ -392,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:$") @@ -436,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) @@ -482,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:$") @@ -496,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) @@ -575,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}) @@ -618,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) @@ -638,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) @@ -693,8 +769,11 @@ if(BUILD_TESTING) target_link_libraries(guest_itimer_tests PRIVATE libkernel libc) configure_windows_unwind(guest_itimer_tests) add_test(NAME guest_itimer COMMAND guest_itimer_tests) + add_test(NAME guest_itimer_pending_default COMMAND guest_itimer_tests pending-default) + set_tests_properties(guest_itimer_pending_default PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "SIGALRM: a pending SIGALRM was unblocked with the default disposition; the process terminates") if(WIN32) - set_tests_properties(guest_itimer PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_itimer guest_itimer_pending_default PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_parent_process_tests ${LIBS_INCLUDE_DIR}/tests/GuestParentProcess.cpp) @@ -759,6 +838,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:$") @@ -949,6 +1029,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) @@ -1169,6 +1259,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) @@ -1220,6 +1322,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) @@ -1373,6 +1485,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}) @@ -1383,6 +1505,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) @@ -1399,8 +1531,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 4dbce761d5..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 @@ -1041,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) { @@ -1084,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/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index a17307030f..3178ca0ca9 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -37,6 +37,7 @@ size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE, bool exclusive = false); int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment); +void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len); int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object); int DoMprotect(const void* addr, size_t len, int prot); int DoMtypeprotect(const void* addr, size_t len, int type, int prot); @@ -49,4 +50,4 @@ void ForgetJitSharedMemory(int descriptor); void DuplicateJitSharedMemory(int from, int to); void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpu_entries, std::uint64_t* gpu_entries); -#endif \ No newline at end of file +#endif diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 658129faa1..ebf8d8a813 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -168,6 +168,10 @@ void ForgetLockedRange(uintptr_t start, uintptr_t end) { } +void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len) { + _releaseFlexible(address, len); +} + extern "C" { int APS5_VABI sceKernelAllocateDirectMemory(int64_t search_start, int64_t search_end, size_t len, size_t alignment, int memory_type, int64_t* phys_addr_out) { @@ -258,6 +262,7 @@ int32_t APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void** addr_in_out, si int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot) { constexpr int DefinedProtection = 0x3F7; + if (len == 0) return 0; return DoMprotect(addr, len, prot & DefinedProtection); } @@ -382,6 +387,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); } diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 6f2c99760c..377f252298 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 @@ -118,6 +119,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti try { const auto result = DoMapAnon(&mapped, rounded, protection, placement, alignment, exclusive); if (result != 0) return failed(GuestNoMemory); + if (fixedAnonymous) ReleaseFlexibleMappingRange(start, rounded); return mapped; } catch (const std::invalid_argument&) { return failed(fixedAnonymous ? GuestInvalid : GuestNoMemory); @@ -134,6 +136,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) @@ -158,6 +161,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti mapped = address; const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, mappingDescriptor, static_cast(offset), alignment); if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory); + if ((placement & GuestMapFixed) != 0) ReleaseFlexibleMappingRange(reinterpret_cast(mapped), rounded); return mapped; } catch (const std::invalid_argument&) { return failed(GuestInvalid); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 641deca1d4..22d850be74 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; @@ -192,6 +208,16 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { return CollectTriggered(ev, num); } +bool KernelEqueuePrivate::HasTriggeredEvents() { + std::unique_lock lock(m_mutex); + if (m_closed) { + return false; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + return std::any_of(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; }); +} + int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros) { return WaitForEventsUntil(ev, num, micros == 0 ? 0 : TimedWait::DeadlineNanos(micros)); } @@ -281,10 +307,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 +665,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..61096a5136 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -58,8 +59,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; @@ -82,6 +85,7 @@ public: bool HasEvent(uintptr_t ident, int16_t filter); int EditEvent(uintptr_t ident, int16_t filter, const std::function& edit); int GetTriggeredEvents(KernelEvent* ev, int num); + bool HasTriggeredEvents(); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); void RemoveDescriptorEvents(uintptr_t ident); @@ -111,6 +115,7 @@ using KernelEqueueRef = std::shared_ptr; void EqueueDescriptorClosed(int descriptor); void KqueueDescriptorClosed(int descriptor); bool IsKqueueDescriptor(int descriptor); +std::optional KqueueReadable(int descriptor); KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata); void RequireVnodeNotes(uint32_t fflags); void SetVnodeNotes(KernelEqueueEvent& event, uint32_t fflags); diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 1696650661..20859f0b23 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)); @@ -294,6 +317,12 @@ bool IsKqueueDescriptor(int descriptor) { return static_cast(FindKqueue(descriptor)); } +std::optional KqueueReadable(int descriptor) { + const auto queue = FindKqueue(descriptor); + if (!queue) return std::nullopt; + return queue->HasTriggeredEvents(); +} + void KqueueDescriptorClosed(int descriptor) { KernelEqueueRef closed; std::vector queues; @@ -338,7 +367,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/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..81a9018c42 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -14,6 +14,8 @@ 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); +bool IsVnode(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..b20bde40a6 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,27 @@ 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; +} + +bool IsVnode(int fd) { + if (fd < 0 || File::IsRandomDevice(fd)) return false; + if (File::IsArchivedFile(fd)) return true; +#ifdef _WIN32 + if (!DescriptorIsOpen_nid_no_patch(fd)) return false; + return File::DirectoryDescriptorPath(fd) || ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_DISK; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && (S_ISREG(status.st_mode) || S_ISDIR(status.st_mode)); +#endif +} + } 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..0bfedc48da 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,8 +990,13 @@ 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 (!File::DirectoryDescriptorPath(fd)) { + if (WithoutParameterHandler([fd] { return ::_get_osfhandle(fd); }) == -1) return SceErrorFromErrno(GUEST_EBADF); + return SceErrorFromErrno(GUEST_EINVAL); + } if (basep != nullptr) *basep = 0; return File::ReadDirectoryDescriptor(fd, buf, nbytes); } @@ -937,10 +1009,14 @@ 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); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + struct stat status{}; + if (::fstat(fd, &status) != 0) return SceErrorFromErrno(errno); + if (!S_ISDIR(status.st_mode)) return SceErrorFromErrno(GUEST_EINVAL); const off_t base = ::lseek(fd, 0, SEEK_CUR); if (base < 0) return SceErrorFromErrno(errno); if (basep != nullptr) *basep = static_cast(base); @@ -992,7 +1068,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 +1114,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 +1150,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 +1211,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 +1233,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 +1257,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 +1285,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 +1307,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 +1344,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 +1354,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 +1374,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 +1407,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 d6fceb6779..aa9a90cb4b 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -228,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/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 4d03696ff6..8a4c613626 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -18,7 +18,6 @@ struct PthreadBarrierPrivate { namespace { constexpr int PROCESS_PRIVATE = 0; -constexpr int PROCESS_SHARED = 1; } @@ -54,7 +53,7 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPrivate** attr, int pshared) { if (!attr || !*attr) return 22; - if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return 22; + if (pshared != PROCESS_PRIVATE) return 22; (*attr)->pshared = pshared; return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 5c5e64d18c..6dd1e09509 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -6,6 +6,8 @@ namespace { +constexpr int POSIX_PROCESS_PRIVATE = 0; + int toPosix(int result) { if (result == 0) return 0; @@ -64,4 +66,18 @@ 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) return PosixThread::GUEST_EINVAL; + (*attr)->_pshared = pshared; + return 0; +} + +int APS5_VABI pthread_condattr_getpshared_nid_postfix(const PthreadCondattr* attr, int* pshared) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (!pshared) throw std::runtime_error("pthread_condattr_getpshared: null pshared"); + *pshared = (*attr)->_pshared; + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index fbe5145666..bf93cf4691 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -13,7 +13,6 @@ 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; @@ -62,7 +61,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; @@ -99,13 +98,13 @@ int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* a 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; + if (pshared != POSIX_PROCESS_PRIVATE) 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); @@ -113,14 +112,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..c07eadf645 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -12,6 +12,7 @@ namespace { constexpr int GUEST_REALTIME_CLOCK = 0; +constexpr int PROCESS_PRIVATE = 0; int toPosix(int result) { if (result == 0) @@ -36,6 +37,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; @@ -54,13 +57,26 @@ int APS5_VABI pthread_rwlockattr_init_nid_postfix(PthreadRwlockattr* attr) { return toPosix(scePthreadRwlockattrInit(attr)); } +int APS5_VABI pthread_rwlockattr_getpshared_nid_postfix(const PthreadRwlockattr* attr, int* pshared) { + if (!attr || !*attr) throw std::runtime_error("pthread_rwlockattr_getpshared: null attr"); + if (!pshared) throw std::runtime_error("pthread_rwlockattr_getpshared: null pshared"); + *pshared = PROCESS_PRIVATE; + return 0; +} + +int APS5_VABI pthread_rwlockattr_setpshared_nid_postfix(PthreadRwlockattr* attr, int pshared) { + if (pshared != PROCESS_PRIVATE) return PosixThread::GUEST_EINVAL; + if (!attr || !*attr) throw std::runtime_error("pthread_rwlockattr_setpshared: null attr"); + return 0; +} + int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { initializeStatic(rwlock, __func__); return toPosix(scePthreadRwlockRdlock(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 +85,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/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp index 3990f0d293..9101c62da8 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -15,7 +15,6 @@ constexpr int GUEST_EPERM = 1; constexpr int GUEST_ENOMEM = 12; constexpr int GUEST_EBUSY = 16; constexpr int PROCESS_PRIVATE = 0; -constexpr int PROCESS_SHARED = 1; constexpr int SPIN_COUNT = 100000; bool tryAcquire(PthreadSpinlockPrivate* spinlock) { @@ -29,7 +28,7 @@ extern "C" { int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { if (!lock) return PosixThread::GUEST_EINVAL; - if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE) return PosixThread::GUEST_EINVAL; auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; if (!spinlock) return GUEST_ENOMEM; *lock = spinlock; 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 1252760c57..9b049a94f5 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -59,6 +59,7 @@ struct PthreadRwlockPrivate { struct PthreadCondattrPrivate { int _clockid; + int _pshared = 0; }; struct PthreadCondPrivate { @@ -140,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}; @@ -161,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..cba281fd82 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 @@ -784,31 +830,33 @@ 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 == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; + if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; 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/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index b21208982d..9bf3b90265 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -32,5 +32,6 @@ extern "C" void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::Stat extern "C" bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error); extern "C" void KernelSetSocketNativeLookup_nid_no_patch(KernelSocketPoll::NativeLookup lookup); extern "C" bool KernelFindSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); +extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native); extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number); diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 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..6cc428f617 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -3,6 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" @@ -13,6 +15,7 @@ #include #include #include +#include #include #include @@ -42,11 +45,6 @@ bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; } -void UnknownDescriptor(int descriptor) { - const auto what = "select on descriptor " + std::to_string(descriptor) + " (not a libSceNet socket)"; - NotImplemented_nid_no_patch(what.c_str()); -} - void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } @@ -61,6 +59,21 @@ int KernelSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMillis return GuestSockets::Poll(entries, count, timeoutMilliseconds); } +int Kqueues(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMilliseconds); + for (;;) { + int ready = 0; + for (int index = 0; index < count; ++index) { + auto& entry = entries[index]; + const auto readable = KqueueReadable(entry.descriptor); + entry.revents = !readable ? KernelSocketPoll::Unknown : *readable ? static_cast(entry.events & KernelSocketPoll::Readable) : short{0}; + if (entry.revents != 0) ++ready; + } + if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; + PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(1))); + } +} + #ifndef _WIN32 short HostEventFlags(short events) { @@ -104,13 +117,11 @@ int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMillise for (int index = 0; index < count; ++index) { auto& entry = descriptors[index]; if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) return -GuestEbadf; - if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) { - NotImplemented_nid_no_patch(("select on descriptor " + std::to_string(entry.descriptor) + " (neither a socket nor a pipe)").c_str()); - } + if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) entry.revents = KernelSocketPoll::Unknown; 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))); } } @@ -180,6 +191,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return -1; } PollGroup kernel{&KernelSockets, {}, {}}; + PollGroup kqueues{&Kqueues, {}, {}}; PollGroup net{nullptr, {}, {}}; std::vector devices; std::vector requested; @@ -193,8 +205,13 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* kernel.entries.push_back({descriptor, events, 0}); kernel.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); - } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); + } else if (IsKqueueDescriptor(descriptor)) { + kqueues.entries.push_back({descriptor, events, 0}); + kqueues.slots.push_back(requested.size()); + requested.push_back({descriptor, events, 0}); + } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast((events & KernelSocketPoll::Readable) != 0 ? KernelSocketPoll::Readable : events)}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); + else if (File::IsVnode(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); else { net.entries.push_back({descriptor, events, 0}); net.slots.push_back(requested.size()); @@ -205,6 +222,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* const bool devicesReady = std::any_of(devices.begin(), devices.end(), [](const auto& entry) { return entry.revents != 0; }); std::vector groups; if (!kernel.entries.empty()) groups.push_back(std::move(kernel)); + if (!kqueues.entries.empty()) groups.push_back(std::move(kqueues)); if (!net.entries.empty()) { PollGroup hosts{&HostPoll, {}, {}}; net.poll = g_socketPoller.load(); @@ -228,7 +246,18 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* net.slots.erase(net.slots.begin() + static_cast(index)); } if (!net.entries.empty()) groups.push_back(std::move(net)); - if (!hosts.entries.empty()) groups.push_back(std::move(hosts)); + if (!hosts.entries.empty()) { + const int probe = HostPoll(hosts.entries.data(), static_cast(hosts.entries.size()), 0); + if (probe < 0) { + *__error_nid_postfix() = -probe; + return -1; + } + for (const auto& entry : hosts.entries) { + if ((entry.revents & KernelSocketPoll::Unknown) != 0) + throw std::runtime_error("select: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file, a directory nor a kqueue"); + } + groups.push_back(std::move(hosts)); + } } const auto start = std::chrono::steady_clock::now(); const auto deadline = limit == nullptr ? std::chrono::steady_clock::time_point::max() @@ -261,9 +290,19 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if (groups.size() == 1 || groupReady > 0 || std::chrono::steady_clock::now() >= sliceEnd) break; } } + if (std::any_of(requested.begin(), requested.end(), [](const auto& entry) { return (entry.revents & KernelSocketPoll::Unknown) != 0; })) { + for (const auto& entry : requested) { + if ((entry.revents & KernelSocketPoll::Unknown) == 0 || DescriptorIsOpen_nid_no_patch(entry.descriptor)) continue; + *__error_nid_postfix() = GuestEbadf; + return -1; + } + if (limit == nullptr) return select_nid_postfix(nfds, readfds, writefds, exceptfds, nullptr); + const auto remaining = std::max(0, std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count()); + const GuestTimeval rest{remaining / 1000000, remaining % 1000000}; + return select_nid_postfix(nfds, readfds, writefds, exceptfds, &rest); + } int ready = 0; for (const auto& entry : requested) { - if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor); const bool readable = (entry.events & KernelSocketPoll::Readable) != 0 && (entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) != 0; const bool writable = (entry.events & KernelSocketPoll::Writable) != 0 && (entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) != 0; const bool urgent = (entry.events & KernelSocketPoll::Urgent) != 0 && (entry.revents & KernelSocketPoll::Urgent) != 0; diff --git a/core/libs/prx/libkernel/Socket/src/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/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7588023bb7..5b79792d75 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 @@ -26,8 +29,10 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.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 +47,6 @@ #include #include #include -#include #include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -122,6 +126,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 +251,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 +348,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 +358,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 +383,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 +405,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); @@ -432,6 +471,7 @@ std::shared_ptr Find(int descriptor) { } constexpr short GuestPollReadable = 0x1 | 0x40; constexpr short GuestPollWritable = 0x4; +constexpr short GuestPollVnodeReady = GuestPollReadable | GuestPollWritable; constexpr short GuestPollHangUp = 0x10; constexpr short GuestPollError = 0x8; [[noreturn]] void ThrowReadEndWritable(int descriptor) { @@ -459,18 +499,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; @@ -701,7 +745,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 @@ -718,6 +762,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; @@ -730,6 +778,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; @@ -775,6 +842,15 @@ int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutM return ready; } +extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { + if (auto socket = Find(descriptor)) { + *native = static_cast(socket->value); + *owner = std::move(socket); + return true; + } + return KernelFindSocketNative_nid_no_patch(descriptor, owner, native); +} + extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { if (!GuestSockets::IsOpen(descriptor)) { @@ -883,7 +959,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); @@ -956,6 +1034,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); @@ -1345,13 +1427,29 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c ++ready; continue; } + if (File::IsRandomDevice(entry.descriptor)) { + const short readable = static_cast(entry.events & GuestPollReadable); + entry.revents = readable != 0 ? readable : entry.events; + if (entry.revents) ++ready; + continue; + } + if (entry.descriptor <= 2) { + entry.revents = static_cast(entry.descriptor == 0 ? 0 : entry.events & GuestPollWritable); + if (entry.revents) ++ready; + continue; + } + if (File::IsVnode(entry.descriptor)) { + entry.revents = static_cast(entry.events & GuestPollVnodeReady); + if (entry.revents) ++ready; + continue; + } #ifdef _WIN32 short revents = 0; if (!PipeReadiness(entry.descriptor, entry.events, revents)) #else if (!IsPipe(entry.descriptor, entry.events)) #endif - throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket nor a pipe"); + throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file nor a directory"); pipes.push_back(i); continue; } @@ -1413,8 +1511,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..3bc229914c 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -11,6 +11,7 @@ static std::atomic g_coredumpHandler{0}; static std::atomic g_coredumpContext{0}; +static constexpr int SCE_COREDUMP_ERROR_INVALID_ARGUMENT = static_cast(0x81180000u); static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast(0x81180003u); static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) { @@ -55,9 +56,10 @@ int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) { return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size) { + (void)size; + if (info == nullptr) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) { @@ -93,15 +95,19 @@ int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { } int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { - (void)threadContextInfo; - (void)threadContextInfoSize; - NotImplemented_nid_no_patch(__func__); + 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(uint32_t first, const char* text, int third) { + if (text == nullptr || first > 15 || third < 0 || third > 256) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceCoredumpWriteUserString() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpConfigDumpMode(int mode) { + return mode == 1 ? 0 : SCE_COREDUMP_ERROR_INVALID_ARGUMENT; } int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) { 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 23f4e7f977..36678ba196 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -37,6 +37,7 @@ using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; +constexpr int GuestSigalrm = 14; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; @@ -112,8 +113,7 @@ bool TakePending(int guest) { auto& word = pending.words[SignalWord(guest)]; const std::uint32_t bit = SignalBit(guest); if ((word.load(std::memory_order_relaxed) & bit) == 0) return false; - word.fetch_and(~bit, std::memory_order_relaxed); - return true; + return (word.fetch_and(~bit, std::memory_order_relaxed) & bit) != 0; } bool PendingInSet(const GuestSignalSet& set, int* guest) { @@ -187,8 +187,18 @@ bool CaughtByGuest(int guest) { return Disposition(guest).handler > 1; } +bool TerminatesWithoutHandler(int guest) { + return guest == GuestSigalrm && Disposition(guest).handler == 0; +} + +[[noreturn]] void TerminateBySigalrm(const char* cause) { + std::fprintf(stderr, "SIGALRM: %s with the default disposition; the process terminates\n", cause); + std::fflush(stderr); + std::_Exit(128 + GuestSigalrm); +} + bool Deliverable(int guest) { - return NativeSignal(guest) != 0 || CaughtByGuest(guest); + return NativeSignal(guest) != 0 || CaughtByGuest(guest) || TerminatesWithoutHandler(guest); } bool TakePendingUnblocked(int* guest) { @@ -205,6 +215,7 @@ bool DeliverPendingSignal(int guest) { const GuestSigaction action = Disposition(guest); if (action.handler == 1) return false; if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("signal: SA_SIGINFO handlers are not delivered"); + if (guest == GuestSigalrm && action.handler == 0) TerminateBySigalrm("a pending SIGALRM was unblocked"); const int native = NativeSignal(guest); if (native != 0) { if (std::raise(native) != 0) throw std::runtime_error("signal: delivering the signal to the host failed"); @@ -260,7 +271,6 @@ extern "C" int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* } namespace { -constexpr int GuestSigalrm = 14; struct AlarmState { std::mutex lock; std::condition_variable changed; @@ -288,13 +298,14 @@ void ExpireAlarm(std::chrono::microseconds interval) { handler = dispositions[GuestSigalrm].handler; } if (handler == 1) return; - 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"); - std::fflush(stderr); - std::_Exit(128 + GuestSigalrm); + if (SignalBlocked(GuestSigalrm)) { + MarkPending(GuestSigalrm); + std::atomic_thread_fence(std::memory_order_seq_cst); + if (SignalBlocked(GuestSigalrm) || !TakePending(GuestSigalrm)) return; + handler = Disposition(GuestSigalrm).handler; + if (handler == 1) return; } + if (handler == 0) TerminateBySigalrm("the alarm expired"); throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired: delivering SIGALRM to a guest handler is not implemented"); } @@ -359,11 +370,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(); { @@ -526,6 +540,7 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { + if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } { std::lock_guard lock(registration); if (previousSet != nullptr) { @@ -546,10 +561,10 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest 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"); } } } + std::atomic_thread_fence(std::memory_order_seq_cst); for (int guest = 1; guest <= MaxSignal; ++guest) { if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; DeliverPendingSignal(guest); 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..12523a767b 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,10 @@ #include #include #include +#ifdef __linux__ +#include +#include +#endif // __linux__ #endif extern "C" int* APS5_VABI __error_nid_postfix(); @@ -179,6 +184,39 @@ std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency() { return 1000000000ULL; } +static std::string DescribeCallSite(const void* caller) { + const auto address = reinterpret_cast(caller); + char text[192]; +#ifdef _WIN32 + const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); + std::snprintf(text, sizeof(text), "exe+0x%llx", static_cast(address - image)); +#elif defined(__linux__) + Dl_info info{}; + link_map* map = nullptr; + if (dladdr1(caller, &info, reinterpret_cast(&map), RTLD_DL_LINKMAP) == 0 || map == nullptr) { + std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); + return text; + } + const char* path = map->l_name; + const char* slash = path == nullptr ? nullptr : std::strrchr(path, '/'); + const char* module = path == nullptr || *path == '\0' ? "exe" : slash == nullptr ? path : slash + 1; + std::snprintf(text, sizeof(text), "%s+0x%llx", module, static_cast(address - map->l_addr)); +#else + std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); +#endif + return text; +} + +static unsigned long CurrentThreadIdForTrace() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#elif defined(__linux__) + return static_cast(gettid()); +#else + return 0; +#endif +} + // Debug aid: APS5_TRACE_USLEEP reports every 2000 calls which guest call sites sleep, so a CPU // thread polling for GPU or I/O completion can be found (offsets are relative to the executable). static void TraceSleep(const void* caller, std::uint64_t microseconds) { @@ -192,15 +230,10 @@ static void TraceSleep(const void* caller, std::uint64_t microseconds) { ++site.first; site.second += microseconds; if (++calls % 2000 != 0) return; -#ifdef _WIN32 - const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); -#else - const std::uintptr_t image = 0; -#endif std::vector>> hot(sites.begin(), sites.end()); std::sort(hot.begin(), hot.end(), [](const auto& a, const auto& b) { return a.second.first > b.second.first; }); std::fprintf(stderr, "[usleep] %llu calls:", static_cast(calls)); - for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " exe+0x%llx x%llu (%llu us)", static_cast(hot[i].first - image), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); + for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " %s x%llu (%llu us)", DescribeCallSite(reinterpret_cast(hot[i].first)).c_str(), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); std::fprintf(stderr, "\n"); } @@ -211,16 +244,13 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static std::mutex mutex; static std::unordered_map sites; static std::uint64_t lastReport = 0; - char key[96]; -#ifdef _WIN32 - const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); - const unsigned long thread = GetCurrentThreadId(); -#else - const std::uintptr_t image = 0; - const unsigned long thread = 0; -#endif - std::snprintf(key, sizeof(key), "%s exe+0x%llx", kind, static_cast(reinterpret_cast(caller) - image)); + static std::unordered_map callSites; + char key[224]; + const unsigned long thread = CurrentThreadIdForTrace(); std::lock_guard lock(mutex); + auto callSite = callSites.find(caller); + if (callSite == callSites.end()) callSite = callSites.emplace(caller, DescribeCallSite(caller)).first; + std::snprintf(key, sizeof(key), "%s %s", kind, callSite->second.c_str()); auto& site = sites[key]; ++site.calls; site.timeouts += timedOut ? 1u : 0u; @@ -348,10 +378,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 +428,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 +461,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 +473,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 1886513ca0..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 { @@ -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 bb1361dd5c..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); 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/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/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index e7e80e9e27..2340688d38 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -73,6 +73,7 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char**, std::u int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizesForEach(const char**, std::uint32_t, std::uint32_t*, std::uint64_t*, int*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceAmprAprCommandBufferReadFile(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint32_t, void*, std::uint64_t, std::uint64_t); +std::uint32_t APS5_VABI sceAmprMeasureCommandSizeReadFile(); int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint64_t, std::uint64_t); int APS5_VABI sceAmprAprCommandBufferReadFileScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t); int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t, std::uint64_t); @@ -156,7 +157,8 @@ static void RequireAt(bool value, int line) { namespace { constexpr int invalidArgument = static_cast(0x80020016); -constexpr int bufferFull = static_cast(0x8002001C); +constexpr int permissionDenied = static_cast(0x80020001); +constexpr int busy = static_cast(0x80020010); constexpr std::uint32_t color = 0xFF8040u; struct Recorder { @@ -249,19 +251,36 @@ void TestFullBuffer() { Recorder exact(measured); Require(sceAmprCommandBufferPushMarker(&exact.buffer, marker) == 0); Require(exact.Offset() == measured && exact.Commands() == 1); - Require(sceAmprCommandBufferPopMarker(&exact.buffer) == bufferFull); - Require(sceAmprCommandBufferSetMarker(&exact.buffer, "") == bufferFull); + Require(sceAmprCommandBufferPopMarker(&exact.buffer) == busy); + Require(sceAmprCommandBufferSetMarker(&exact.buffer, "") == busy); Require(exact.Offset() == measured && exact.Commands() == 1); Recorder small(measured - 4); - Require(sceAmprCommandBufferPushMarker(&small.buffer, marker) == bufferFull); - Require(sceAmprCommandBufferSetMarkerWithColor(&small.buffer, marker, &color) == bufferFull); + Require(sceAmprCommandBufferPushMarker(&small.buffer, marker) == busy); + Require(sceAmprCommandBufferSetMarkerWithColor(&small.buffer, marker, &color) == busy); Require(small.Offset() == 0 && small.Commands() == 0); + std::array destination{}; + volatile std::uint64_t done = 0; + const auto read = sceAmprMeasureCommandSizeReadFile(); + Recorder reads(read); + Require(sceAmprAprCommandBufferReadFile(&reads.buffer, &reads.gatherState, &reads.scatterState, 1, destination.data(), destination.size(), 0) == 0); + Require(sceAmprAprCommandBufferReadFile(&reads.buffer, &reads.gatherState, &reads.scatterState, 1, destination.data(), destination.size(), 0) == busy); + Require(sceAmprCommandBufferNop(&reads.buffer, 1) == busy); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&reads.buffer, &done, 1) == busy); + Require(reads.Offset() == read && reads.Commands() == 1); + Apr::CommandBufferObject unbound{}; + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; Require(sceAmprCommandBufferConstructor(&unbound) == 0); - Require(sceAmprCommandBufferPushMarker(&unbound, marker) == bufferFull); - Require(sceAmprCommandBufferPopMarker(&unbound) == bufferFull); + Require(sceAmprAprCommandBufferConstructor(&unbound, &gatherState, &scatterState) == 0); + Require(sceAmprCommandBufferPushMarker(&unbound, marker) == permissionDenied); + Require(sceAmprCommandBufferPopMarker(&unbound) == permissionDenied); + Require(sceAmprAprCommandBufferReadFile(&unbound, &gatherState, &scatterState, 1, destination.data(), destination.size(), 0) == permissionDenied); + Require(sceAmprCommandBufferNop(&unbound, 1) == permissionDenied); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&unbound, &done, 1) == permissionDenied); + Require(unbound.offset == 0 && unbound.numCommands == 0); } void TestOffsetPastBuffer() { @@ -878,8 +897,6 @@ void TestGatherScatter() { } constexpr std::uint64_t page = 0x4000; -constexpr int permissionDenied = static_cast(0x80020001); -constexpr int busy = static_cast(0x80020010); constexpr int noSuchSubmission = static_cast(0x80020003); constexpr std::int32_t cpuReadWrite = 0x03; constexpr std::int32_t cpuGpuReadWrite = 0x33; 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/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/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 07ccf6b0b3..7dec7aabab 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -147,30 +147,44 @@ void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const Gu GuestLocale::String result; std::memset(&result, 0xcd, sizeof(result)); require(TransformWide(wide, &result, text) == &result); - require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 6 && result.capacity == 14 && allocations == before + 1); + 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 == 14 && reinterpret_cast(result.buffer)[0] == 0); + 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 == 14 && result.capacity == 14 && allocations == before + 1); + 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 == 33 && result.size == 16 && result.capacity == 31 && result.pointer == lastAllocation); + 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 + 2); + 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())); }); @@ -183,7 +197,7 @@ void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const Gu require(frees == freed); Vtable(facet).facet.deleteObject(facet); require(frees == freed + 1 && lastFree == wide); - require(allocations == before + 2); + require(allocations == before + 4); } } @@ -284,6 +298,6 @@ int main() { require(frees == 1); Vtable(facet).facet.deleteObject(facet); require(frees == 2 && lastFree == collate); - require(allocations == 6); + 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..43e32238e0 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -13,9 +13,14 @@ 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); +int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size); +int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third); +int APS5_VABI sceCoredumpConfigDumpMode(int mode); } namespace { +constexpr int InvalidArgument = static_cast(0x81180000u); constexpr int NotInCoredumpHandler = static_cast(0x81180003u); constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u; bool handlerRan = false; @@ -40,11 +45,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 +64,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 +81,31 @@ 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); + + Require(sceCoredumpGetStopInfoGpu_Agc(region.data() + 8, 8) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoGpu_Agc(region.data() + 8, 3) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoGpu_Agc(nullptr, 8) == InvalidArgument); + Require(sceCoredumpWriteUserString(1, "text", 4) == 0); + Require(sceCoredumpWriteUserString(0, "", 0) == 0); + Require(sceCoredumpWriteUserString(1, nullptr, 4) == InvalidArgument); + Require(sceCoredumpWriteUserString(15, "x", 256) == 0 && sceCoredumpWriteUserString(0, "x", 0) == 0); + Require(sceCoredumpWriteUserString(16, "x", 1) == InvalidArgument && sceCoredumpWriteUserString(0x80000000u, "x", 1) == InvalidArgument); + Require(sceCoredumpWriteUserString(1, "x", 257) == InvalidArgument && sceCoredumpWriteUserString(1, "x", -1) == InvalidArgument); + Require(region == original); + Require(sceCoredumpConfigDumpMode(1) == 0); + for (const int mode : {0, 2, -1}) Require(sceCoredumpConfigDumpMode(mode) == InvalidArgument); 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/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index bfd1beb0dd..8ceba21364 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -66,7 +66,7 @@ static void CheckName(const std::u8string& utf8Name) { Require(sceKernelGetdents(directory, buffer.data(), 8) == 0); Require(sceKernelGetdents(directory, buffer.data(), 256) == 0); Require(sceKernelClose(directory) == 0); - Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_ENOTDIR); + Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_EBADF); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestDirectoryClose.cpp b/core/libs/tests/GuestDirectoryClose.cpp index b4ab7b0fc2..68d3d60176 100644 --- a/core/libs/tests/GuestDirectoryClose.cpp +++ b/core/libs/tests/GuestDirectoryClose.cpp @@ -25,7 +25,7 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -static constexpr int ErrorEnotdir = static_cast(0x80020014u); +static constexpr int ErrorEinval = static_cast(0x80020016u); static void VerifyCloseReuse(const std::filesystem::path& root, const char* scenario, bool useUnderscoreClose) { @@ -49,7 +49,7 @@ static void VerifyCloseReuse(const std::filesystem::path& root, const char* scen Require(file == directory); std::array entries{}; - Require(sceKernelGetdents(file, entries.data(), static_cast(entries.size())) == ErrorEnotdir); + Require(sceKernelGetdents(file, entries.data(), static_cast(entries.size())) == ErrorEinval); Require((useUnderscoreClose ? _close_nid_postfix(file) : close_nid_postfix(file)) == 0); } 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..fe592aa93a 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifdef _WIN32 #include @@ -55,6 +56,8 @@ int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelOpen(const char*, int, unsigned short); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); +int APS5_VABI getdents_nid_postfix(int, char*, int); +int APS5_VABI getdirentries_nid_postfix(int, char*, int, int64_t*); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelReadv(int, const void*, int); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); @@ -249,6 +252,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 +342,9 @@ int main() { TestParentTraversal(); TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); +#ifdef _WIN32 + TestLongHostPathStat(); +#endif #ifdef __linux__ TestMissingPathListsDirectoryOnce(); #endif @@ -304,6 +359,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); @@ -543,7 +599,13 @@ int main() { Require(rename_nid_postfix(name(replacement).c_str(), name(dangling).c_str()) == 0); Require(!std::filesystem::is_symlink(dangling) && std::filesystem::is_regular_file(dangling)); #endif - std::filesystem::remove_all(area); + std::error_code cleanupError; + for (int attempt = 0; attempt < 10; ++attempt) { + std::filesystem::remove_all(area, cleanupError); + if (!cleanupError) break; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + Require(!cleanupError); } Require(remove_nid_postfix(file.string().c_str()) == 0); Require(!std::filesystem::exists(file)); @@ -833,6 +895,13 @@ int main() { Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast(0x80020009u)); std::vector listing(4096); Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast(listing.size())) > 0); + const int plainFile = open_nid_postfix(presentName.c_str(), 0, 0); + Require(plainFile >= 0); + Require(sceKernelGetdents(plainFile, listing.data(), static_cast(listing.size())) == static_cast(0x80020016u)); + Require(getdents_nid_postfix(plainFile, listing.data(), static_cast(listing.size())) == -1 && *__error_nid_postfix() == 22); + int64_t plainBase = 0; + Require(getdirentries_nid_postfix(plainFile, listing.data(), static_cast(listing.size()), &plainBase) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(plainFile) == 0); Require(close_nid_postfix(directoryDescriptor) == 0); const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0); Require(flaggedDirectory >= 0 && sceKernelClose(flaggedDirectory) == 0); 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/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 85af8b0350..8ec9cbeb30 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -1,17 +1,18 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" +#include "prx/libSceHmd2/Hmd2.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include #include +#include #include -extern "C" { -std::int32_t APS5_VABI sceHmd2Initialize(const void* param); -int APS5_VABI sceHmd2Open(); -} - namespace { +alignas(16) std::array process{}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "HMD2: %s\n", message); @@ -19,19 +20,120 @@ void Require(bool condition, const char* message) { } } +void SetSdkVersion(std::uint32_t version) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); } -int main() { +void InstallProcessParameters() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +bool IsDefaultFieldOfView(const Hmd2::FieldOfView& fieldOfView) { + return fieldOfView.tanOut == 1.84177089f && fieldOfView.tanIn == 0.947190642f && + fieldOfView.tanTop == 1.32898104f && fieldOfView.tanBottom == 1.32898104f; +} + +void RequireNotDetected(const Hmd2::DeviceInformation& info, std::int32_t userId) { + Require(info.status == Hmd2::DeviceStatus::NotDetected, "device status is not NOT_DETECTED"); + Require(info.userId == userId, "device user does not match the SDK version"); + Require(info.panelWidth == 4000 && info.panelHeight == 2040, "panel resolution differs from the firmware"); + Require(info.latency90Hz == 5555 && info.latency120Hz == 4160, "flip latencies differ from the firmware"); + Require(info.hmuMount == 0 && info.lensSeparationDistance == 0, "a missing headset reported a mount or lens separation"); + Require(info.reserved[0] == 0 && info.reserved[1] == 0, "reserved bytes were not cleared"); + Require(info.virtualImageDistance == 2.0f, "virtual image distance differs from the firmware"); +} + +void TestBeforeInitialize() { + Hmd2::DeviceInformation info; + std::memset(&info, 0xcc, sizeof(info)); + Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information before initialization"); + const Hmd2::DeviceInformation cleared{}; + Require(std::memcmp(&info, &cleared, sizeof(info)) == 0, "device information was not cleared before the initialization check"); + Require(sceHmd2GetDeviceInformation(nullptr) == Hmd2::ParameterNull, "null device information before initialization"); + Hmd2::FieldOfView fieldOfView{}; + Require(sceHmd2GetFieldOfViewWithoutHandle(&fieldOfView) == Hmd2::NotInitialized, "field of view before initialization"); + Require(sceHmd2GetFieldOfView(0, &fieldOfView) == Hmd2::InvalidHandle, "field of view of handle 0 before initialization"); + Require(sceHmd2GetDeviceInformationByHandle(0, &info) == Hmd2::NotInitialized, "device information of handle 0 before initialization"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); + Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); + Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed"); +} + +void TestInitialized() { const std::uint8_t param[16]{}; - Require(sceHmd2Initialize(param) == static_cast(0x81110016), "initialization did not report the unsupported feature"); - Require(sceHmd2Initialize(nullptr) == static_cast(0x81110016), "initialization without a param did not report the unsupported feature"); + Require(sceHmd2Initialize(nullptr) == Hmd2::ParameterNull, "initialization without a param"); + Require(sceHmd2Initialize(param) == 0, "initialization failed"); + Require(sceHmd2Initialize(param) == Hmd2::AlreadyInitialized, "second initialization"); + + Hmd2::DeviceInformation info; + std::memset(&info, 0xcc, sizeof(info)); + SetSdkVersion(0x06000000); + Require(sceHmd2GetDeviceInformation(&info) == 0, "device information failed"); + RequireNotDetected(info, -1); + SetSdkVersion(0x07590001); + std::memset(&info, 0xcc, sizeof(info)); + Require(sceHmd2GetDeviceInformation(&info) == 0, "device information for SDK 7.59 failed"); + RequireNotDetected(info, 0); + + Hmd2::FieldOfView fieldOfView{}; + Require(sceHmd2GetFieldOfViewWithoutHandle(&fieldOfView) == 0 && IsDefaultFieldOfView(fieldOfView), "default field of view"); + Require(sceHmd2GetFieldOfViewWithoutHandle(nullptr) == Hmd2::ParameterNull, "null field of view"); + Require(sceHmd2GetFieldOfView(12345, &fieldOfView) == Hmd2::InvalidHandle, "field of view of an unknown handle"); + Require(sceHmd2GetFieldOfView(12345, nullptr) == Hmd2::ParameterNull, "null field of view of an unknown handle"); + Require(sceHmd2GetDeviceInformationByHandle(12345, &info) == Hmd2::InvalidHandle, "device information of an unknown handle"); + Require(sceHmd2SetVibration(12345, 0) == Hmd2::InvalidHandle, "vibration of an unknown handle"); + Require(sceHmd2Close(12345) == Hmd2::InvalidHandle, "close of an unknown handle"); + + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 1, 0, nullptr) == Hmd2::ParameterInvalid, "open with a device type"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 1, nullptr) == Hmd2::ParameterInvalid, "open with a device index"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, param) == Hmd2::ParameterInvalid, "open with a param"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID + 1, 0, 0, nullptr) == Hmd2::ParameterInvalid, "open for another user on SDK 7.59"); + + const auto handle = sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr); + Require(handle > 0, "open without a headset failed"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::AlreadyOpened, "second open"); + std::memset(&info, 0xcc, sizeof(info)); + Require(sceHmd2GetDeviceInformationByHandle(handle, &info) == 0, "device information by handle failed"); + RequireNotDetected(info, 0); + fieldOfView = {}; + Require(sceHmd2GetFieldOfView(handle, &fieldOfView) == 0 && IsDefaultFieldOfView(fieldOfView), "field of view by handle"); + Require(sceHmd2SetVibration(handle, 26) == 0, "vibration of a disconnected headset failed"); + Require(sceHmd2Close(handle) == 0, "close failed"); + Require(sceHmd2Close(handle) == Hmd2::InvalidHandle, "second close"); + Require(sceHmd2Close(0) == 0, "close of the unopened handle 0 failed"); + + const auto kept = sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr); + Require(kept > 0, "reopen failed"); + Require(sceHmd2Terminate() == 0, "terminate failed"); + Require(sceHmd2Terminate() == Hmd2::NotInitialized, "second terminate"); + Require(sceHmd2GetFieldOfView(kept, &fieldOfView) == Hmd2::NotInitialized, "field of view after terminate"); + Require(sceHmd2Close(kept) == Hmd2::NotInitialized, "close after terminate"); + Require(sceHmd2Initialize(param) == 0, "reinitialization failed"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::AlreadyOpened, "terminate closed the open handle"); + Require(sceHmd2Close(kept) == 0, "close after reinitialization failed"); + + SetSdkVersion(0x06000000); bool threw = false; try { - sceHmd2Open(); + sceHmd2Open(USER_SERVICE_INITIAL_USER_ID + 1, 0, 0, nullptr); } catch (const std::runtime_error&) { threw = true; } - Require(threw, "open after failed initialization did not throw"); + Require(threw, "open for another user before SDK 7.00 did not throw"); + Require(sceHmd2Terminate() == 0, "final terminate failed"); +} + +} + +int main() { + InstallProcessParameters(); + TestBeforeInitialize(); + TestInitialized(); std::puts("HMD2 tests passed"); return 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/GuestInetAddress.cpp b/core/libs/tests/GuestInetAddress.cpp new file mode 100644 index 0000000000..80447cad5d --- /dev/null +++ b/core/libs/tests/GuestInetAddress.cpp @@ -0,0 +1,68 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI inet_aton_nid_postfix(const char* text, std::uint32_t* address); +std::uint32_t APS5_VABI inet_addr_nid_postfix(const char* text); +char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address); +char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size); +} + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +std::uint32_t Network(std::uint8_t a, std::uint8_t b, std::uint8_t c, std::uint8_t d) { + const std::uint8_t bytes[4] = {a, b, c, d}; + std::uint32_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +bool Parses(const char* text, std::uint32_t expected) { + std::uint32_t address = 0x5a5a5a5a; + return inet_aton_nid_postfix(text, &address) == 1 && address == expected; +} + +bool Rejects(const char* text) { + std::uint32_t address = 0x5a5a5a5a; + return inet_aton_nid_postfix(text, &address) == 0 && address == 0x5a5a5a5a; +} + +} + +int main() { + Require(Parses("192.168.1.2", Network(192, 168, 1, 2)), "dotted quad"); + Require(Parses("10.1", Network(10, 0, 0, 1)), "two parts"); + Require(Parses("10.1.300", Network(10, 1, 1, 44)), "three parts"); + Require(Parses("0x7f.1", Network(127, 0, 0, 1)), "hex part"); + Require(Parses("017.0.0.1", Network(15, 0, 0, 1)), "octal part"); + Require(Parses("3232235777", Network(192, 168, 1, 1)), "single number"); + Require(Parses("1.2.3.4 trailing", Network(1, 2, 3, 4)), "trailing after space"); + Require(Parses("0", Network(0, 0, 0, 0)), "zero"); + Require(Parses("0xffffffff", Network(255, 255, 255, 255)), "broadcast"); + Require(Rejects("256.1.1.1") && Rejects("1.2.3.256") && Rejects("1.2.65536"), "part out of range"); + Require(Rejects("1.2.3.4.5") && Rejects("08") && Rejects("0x") && Rejects("1.2.3.4x"), "malformed"); + Require(Rejects("") && Rejects(".1") && Rejects("1..2") && Rejects("-1"), "missing digits"); + Require(inet_aton_nid_postfix("1.2.3.4", nullptr) == 1 && inet_aton_nid_postfix("x", nullptr) == 0, "null destination"); + + Require(inet_addr_nid_postfix("127.0.0.1") == Network(127, 0, 0, 1), "inet_addr"); + Require(inet_addr_nid_postfix("bad") == 0xffffffffu && inet_addr_nid_postfix("255.255.255.255") == 0xffffffffu, "inet_addr none"); + + char* const text = inet_ntoa_nid_postfix(Network(10, 20, 30, 40)); + Require(std::string(text) == "10.20.30.40", "inet_ntoa"); + Require(inet_ntoa_nid_postfix(Network(255, 255, 255, 255)) == text && std::string(text) == "255.255.255.255", "inet_ntoa static buffer"); + char buffer[16]; + Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), buffer, sizeof(buffer)) == buffer && std::string(buffer) == "1.2.3.4", "inet_ntoa_r"); + char tiny[4] = {'k', 'e', 'e', 'p'}; + Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), tiny, sizeof(tiny)) == tiny, "inet_ntoa_r small buffer returns it"); + return 0; +} 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/GuestItimer.cpp b/core/libs/tests/GuestItimer.cpp index 8958441978..4e1e9d5845 100644 --- a/core/libs/tests/GuestItimer.cpp +++ b/core/libs/tests/GuestItimer.cpp @@ -17,6 +17,13 @@ struct GuestItimerval { KernelTimeval value; }; static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +struct GuestTimespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; using Handler = void (APS5_VABI *)(int); using TerminateHandler = void (*)(); @@ -25,6 +32,9 @@ int APS5_VABI setitimer_nid_postfix(int, const GuestItimerval*, GuestItimerval*) int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); int* APS5_VABI __error_nid_postfix(); void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix(); @@ -64,6 +74,19 @@ static void APS5_VABI OnAlarm(int signal) { delivered = signal; } +static bool AlarmPending() { + GuestSignalSet pending{{0, 0, 0, 0}}; + Require(sigpending_nid_postfix(&pending) == 0, "sigpending failed"); + return (pending.bits[0] & (1u << 13)) != 0; +} + +static const GuestItimerval once{{0, 0}, {0, 20000}}; + +static void ExpireOnce() { + Require(setitimer_nid_postfix(0, &once, nullptr) == 0, "arming the blocked timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); +} + static void OnTerminate() { const auto* type = __cxa_current_exception_type_nid_postfix(); const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); @@ -77,7 +100,18 @@ static void OnTerminate() { std::_Exit(1); } -int main() { +static int UnblockPendingDefault() { + const GuestSignalSet alarmSet{{1u << 13, 0, 0, 0}}; + Require(_sigprocmask_nid_postfix(1, &alarmSet, nullptr) == 0, "blocking SIGALRM failed"); + ExpireOnce(); + Require(AlarmPending(), "a blocked expiry with the default disposition was not left pending"); + Require(_sigprocmask_nid_postfix(2, &alarmSet, nullptr) == 0, "unblocking SIGALRM failed"); + std::fprintf(stderr, "unblocking a pending SIGALRM with the default disposition did not terminate\n"); + return 1; +} + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "pending-default") == 0) return UnblockPendingDefault(); const auto ignore = reinterpret_cast(std::uintptr_t{1}); Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); const GuestItimerval valid{{0, 0}, {30, 0}}; @@ -154,6 +188,36 @@ int main() { Require(setitimer_nid_postfix(0, &zero, &previous) == 0 && Micros(previous.value) > 0, "disarming the alarm failed"); Require(GuestAlarm_nid_no_patch(0) == 0, "setitimer did not cancel the alarm"); + const GuestSignalSet alarmSet{{1u << 13, 0, 0, 0}}; + const GuestTimespec poll{0, 0}; + const GuestTimespec patience{2, 0}; + GuestSignalSet unblocked{}; + Require(_sigprocmask_nid_postfix(1, &alarmSet, &unblocked) == 0, "blocking SIGALRM failed"); + ExpireOnce(); + Require(!AlarmPending(), "an ignored expiry was left pending"); + Require(signal_nid_postfix(14, nullptr) == ignore, "SIGALRM was not defaulted"); + Require(setitimer_nid_postfix(0, &once, nullptr) == 0, "arming the waited timer failed"); + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &patience) == 14, "sigtimedwait did not take the blocked expiry"); + Require(!AlarmPending(), "the taken expiry is still pending"); + const GuestItimerval frequent{{0, 20000}, {0, 20000}}; + Require(setitimer_nid_postfix(0, &frequent, nullptr) == 0, "arming the frequent timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(setitimer_nid_postfix(0, &zero, nullptr) == 0, "disarming the frequent timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &poll) == 14, "the repeated expiries were not left pending"); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &poll) == -1 && *__error_nid_postfix() == 35, + "the repeated expiries were not merged into one pending signal"); + Require(signal_nid_postfix(14, OnAlarm) == nullptr, "SIGALRM handler was not installed while blocked"); + ExpireOnce(); + Require(delivered == 0 && AlarmPending(), "a blocked expiry with a handler was not left pending"); + Require(_sigprocmask_nid_postfix(3, &unblocked, nullptr) == 0, "unblocking SIGALRM failed"); + Require(delivered == 14, "unblocking SIGALRM did not run its handler"); + Require(_sigprocmask_nid_postfix(1, &alarmSet, nullptr) == 0 && !AlarmPending(), "the delivered expiry is still pending"); + Require(_sigprocmask_nid_postfix(3, &unblocked, nullptr) == 0, "restoring the signal mask failed"); + delivered = 0; + Require(signal_nid_postfix(14, ignore) == OnAlarm, "SIGALRM was not ignored again"); + Require(signal_nid_postfix(14, OnAlarm) == ignore, "SIGALRM handler was not installed"); _ZSt13set_terminatePFvvE_nid_postfix(OnTerminate); const GuestItimerval handled{{0, 250000}, {0, 50000}}; 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/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/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 ed422b3b41..d095eda0ff 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); @@ -116,6 +117,34 @@ int APS5_VABI __isinf_nid_postfix(double); int APS5_VABI isnan_nid_postfix(double); int APS5_VABI isinf_nid_postfix(double); std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); +std::lldiv_t APS5_VABI imaxdiv_nid_postfix(std::int64_t, std::int64_t); +std::int64_t APS5_VABI labs_nid_postfix(std::int64_t); +std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t); +std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t); +long double APS5_VABI scalbnl_nid_postfix(long double, int); +float APS5_VABI significandf_nid_postfix(float); +long double APS5_VABI acoshl_nid_postfix(long double); +long double APS5_VABI asinhl_nid_postfix(long double); +long double APS5_VABI atanhl_nid_postfix(long double); +long double APS5_VABI atan2l_nid_postfix(long double, long double); +long double APS5_VABI cbrtl_nid_postfix(long double); +long double APS5_VABI erfl_nid_postfix(long double); +long double APS5_VABI erfcl_nid_postfix(long double); +long double APS5_VABI expm1l_nid_postfix(long double); +long double APS5_VABI log1pl_nid_postfix(long double); +long double APS5_VABI log2l_nid_postfix(long double); +long double APS5_VABI lgammal_nid_postfix(long double); +long double APS5_VABI tgammal_nid_postfix(long double); +long double APS5_VABI hypotl_nid_postfix(long double, long double); +float APS5_VABI hypot3f_nid_postfix(float, float, float); +double APS5_VABI hypot3_nid_postfix(double, double, double); +long double APS5_VABI hypot3l_nid_postfix(long double, long double, long double); +float APS5_VABI j0f_nid_postfix(float); +float APS5_VABI j1f_nid_postfix(float); +float APS5_VABI jnf_nid_postfix(int, float); +float APS5_VABI y0f_nid_postfix(float); +float APS5_VABI y1f_nid_postfix(float); +float APS5_VABI ynf_nid_postfix(int, float); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); double APS5_VABI nan_nid_postfix(const char*); @@ -754,6 +783,67 @@ static void CheckFloatMathExports() { Require(isinf_nid_postfix(nan) == 0 && isinf_nid_postfix(1.0) == 0); } +static void CheckIntegerAbsAndDivision() { + for (auto function : {labs_nid_postfix, llabs_nid_postfix, imaxabs_nid_postfix}) { + Require(function(-5) == 5 && function(7) == 7 && function(0) == 0); + Require(function(-0x100000000ll) == 0x100000000ll); + Require(function(INT64_MAX) == INT64_MAX && function(INT64_MIN) == INT64_MIN); + } + const auto small = imaxdiv_nid_postfix(-7, 2); + Require(small.quot == -3 && small.rem == -1); + const auto large = imaxdiv_nid_postfix(0x123456789abll, -0x1000); + Require(large.quot == -0x12345678ll && large.rem == 0x9abll); +} + +static void CheckScaling() { + Require(scalbnl_nid_postfix(3.0L, 4) == 48.0L && scalbnl_nid_postfix(1.0L, -16445) == 0x1p-16445L); + Require(scalbnl_nid_postfix(1.0L, 16384) == std::numeric_limits::infinity()); + Require(significandf_nid_postfix(12.0f) == 1.5f && significandf_nid_postfix(-0.375f) == -1.5f); + Require(significandf_nid_postfix(0x1p-149f) == 1.0f); + Require(std::signbit(significandf_nid_postfix(-0.0f)) && significandf_nid_postfix(-0.0f) == 0.0f); + Require(significandf_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(significandf_nid_postfix(std::numeric_limits::quiet_NaN()))); +} + +static bool Near(long double actual, long double expected) { + return std::fabs(actual - expected) <= std::fabs(expected) * 1e-17L; +} + +static void CheckLongDoubleFunctions() { + const long double infinity = std::numeric_limits::infinity(); + const long double nan = std::numeric_limits::quiet_NaN(); + Require(acoshl_nid_postfix(1.0L) == 0.0L && std::isnan(acoshl_nid_postfix(0.5L))); + Require(asinhl_nid_postfix(-0.0L) == 0.0L && std::signbit(asinhl_nid_postfix(-0.0L))); + Require(atanhl_nid_postfix(1.0L) == infinity && Near(atanhl_nid_postfix(0.5L), 0.5493061443340548456976226L)); + Require(Near(atan2l_nid_postfix(1.0L, 1.0L), 0.7853981633974483096156608L) && Near(atan2l_nid_postfix(0.0L, -1.0L), 3.1415926535897932384626434L)); + Require(cbrtl_nid_postfix(-27.0L) == -3.0L && Near(cbrtl_nid_postfix(0x1p-16443L) * cbrtl_nid_postfix(0x1p-16443L) * cbrtl_nid_postfix(0x1p-16443L), 0x1p-16443L)); + Require(erfl_nid_postfix(0.0L) == 0.0L && erfl_nid_postfix(infinity) == 1.0L && erfcl_nid_postfix(0.0L) == 1.0L); + Require(erfcl_nid_postfix(infinity) == 0.0L && erfcl_nid_postfix(-infinity) == 2.0L); + Require(expm1l_nid_postfix(0.0L) == 0.0L && expm1l_nid_postfix(-infinity) == -1.0L); + Require(log1pl_nid_postfix(0.0L) == 0.0L && log1pl_nid_postfix(-1.0L) == -infinity); + Require(log2l_nid_postfix(8.0L) == 3.0L && log2l_nid_postfix(0x1p-16445L) == -16445.0L); + Require(lgammal_nid_postfix(1.0L) == 0.0L && lgammal_nid_postfix(2.0L) == 0.0L && lgammal_nid_postfix(0.0L) == infinity); + Require(tgammal_nid_postfix(5.0L) == 24.0L && Near(tgammal_nid_postfix(0.5L), 1.7724538509055160272981675L)); + Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); + + Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); + Require(hypot3f_nid_postfix(2.0f, 0.0f, 0.0f) == 2.0f && hypot3f_nid_postfix(-2.0f, 3.0f, 6.0f) == 7.0f); + Require(hypot3_nid_postfix(1.0, 2.0, 2.0) == 3.0 && hypot3_nid_postfix(12.0, 3.0, 4.0) == 13.0); + Require(hypot3l_nid_postfix(2.0L, -3.0L, 6.0L) == 7.0L); + const double payload = std::bit_cast(0x7ff8000000000123ull); + Require(std::bit_cast(hypot3_nid_postfix(1.0, payload, std::numeric_limits::infinity())) == 0x7ff8000000000123ull); + Require(std::isnan(hypot3f_nid_postfix(std::numeric_limits::infinity(), 1.0f, std::numeric_limits::quiet_NaN()))); + Require(hypot3l_nid_postfix(-infinity, 1.0L, 2.0L) == infinity && hypot3_nid_postfix(1.0, -std::numeric_limits::infinity(), 0.0) == std::numeric_limits::infinity()); + Require(hypot3_nid_postfix(0x1p1000, 0x1p1000, 0x1p1000) == std::hypot(0x1p1000, std::hypot(0x1p1000, 0x1p1000))); + + Require(j0f_nid_postfix(0.0f) == 1.0f && j1f_nid_postfix(0.0f) == 0.0f && jnf_nid_postfix(2, 0.0f) == 0.0f); + Require(std::fabs(j0f_nid_postfix(2.5f) - static_cast(std::cyl_bessel_j(0.0, 2.5))) < 1e-6f); + Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast(std::cyl_bessel_j(3.0, 4.0))) < 1e-6f); + Require(y0f_nid_postfix(0.0f) == -std::numeric_limits::infinity() && y1f_nid_postfix(0.0f) == -std::numeric_limits::infinity()); + Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast(std::cyl_neumann(2.0, 3.0))) < 1e-6f); + Require(std::isnan(y0f_nid_postfix(-1.0f))); +} + static void CheckDoubleClassification() { const struct { std::uint64_t bits; short code; } cases[] = { {0x0000000000000000ull, 0}, {0x8000000000000000ull, 0}, @@ -932,6 +1022,15 @@ int main() { 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}) { @@ -1083,4 +1182,7 @@ int main() { CheckHypotSpecialValue(); CheckFmodErrno(); CheckDoubleClassification(); + CheckIntegerAbsAndDivision(); + CheckScaling(); + CheckLongDoubleFunctions(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 0f148915e1..b7bab77cac 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -922,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")); @@ -936,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(); @@ -1042,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() { @@ -1217,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; @@ -2485,6 +2557,58 @@ static void CheckPosixFixedAnonymousMapping() { Require(munmap_nid_postfix(memory, page * 4) == 0); } +static void CheckPosixFixedAnonymousReleasesFlexibleAccounting() { + constexpr std::size_t page = 0x4000; + constexpr int fixed = 0x1012; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 3, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 3); + auto* bytes = static_cast(base); + bytes[0] = 0x31; + bytes[page] = 0x41; + bytes[page * 2] = 0x51; + Require(mmap_nid_postfix(bytes + page, page, 3, fixed, -1, 0) == bytes + page); + Require(bytes[0] == 0x31 && bytes[page] == 0 && bytes[page * 2] == 0x51); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + Require(sceKernelMunmap(base, page * 3) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + +#if defined(__linux__) +static void CheckPosixFixedFileReleasesFlexibleAccounting() { + constexpr std::size_t page = 0x4000; + constexpr int fixedShared = 0x11; + const auto path = std::filesystem::path("anyps5-fixed-file-accounting-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + { + std::ofstream stream(path, std::ios::binary); + std::vector data(page, 0x6d); + stream.write(data.data(), static_cast(data.size())); + } + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 3, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 3); + auto* bytes = static_cast(base); + bytes[0] = 0x31; + bytes[page * 2] = 0x51; + Require(mmap_nid_postfix(bytes + page, page, 1, fixedShared, fd, 0) == bytes + page); + Require(bytes[0] == 0x31 && bytes[page] == 0x6d && bytes[page * 2] == 0x51); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + Require(sceKernelMunmap(base, page * 3) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + Require(sceKernelClose(fd) == 0); + std::filesystem::remove(path); +} +#endif + static void CheckSceKernelMmap() { constexpr std::size_t page = 0x4000; void* mapped = nullptr; @@ -2584,6 +2708,10 @@ int main(int argc, char** argv) { #endif if (argc == 2 && std::strcmp(argv[1], "fixed-anonymous") == 0) { CheckPosixFixedAnonymousMapping(); + CheckPosixFixedAnonymousReleasesFlexibleAccounting(); +#if defined(__linux__) + CheckPosixFixedFileReleasesFlexibleAccounting(); +#endif return 0; } Require(argc == 1); @@ -2630,11 +2758,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/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 bc806a3cd8..b25419c969 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -24,22 +24,30 @@ 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); @@ -54,7 +62,7 @@ int main() { Require(sceMsgDialogUpdateStatus() == kStatusFinished); Require(sceMsgDialogGetStatus() == kStatusFinished); - MsgDialogResult result{}; + result = MsgDialogResult{}; result.button_id = -1; Require(sceMsgDialogGetResult(&result) == 0); Require(result.result == 0); @@ -62,6 +70,8 @@ int main() { 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 7bfefee198..a89ed3ea2e 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -4,10 +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" { @@ -73,5 +77,20 @@ int main() { 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/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp new file mode 100644 index 0000000000..5c4eaf025a --- /dev/null +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -0,0 +1,248 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); +int APS5_VABI sceNpTrophy2GetGroupInfoArray(int, int, uint32_t, uint32_t, NpTrophy2GroupDetails*, NpTrophy2GroupData*, uint32_t*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*); +} + +static constexpr int InitialUser = 0x10000000; + +static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}}, + {"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true}, + {"id":"7","grade":"B","groupId":"1"} +]})"; + +static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}], + "groupMetadata":[{"id":"1","name":"Expansion"}]}})"; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +static std::string Ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + PutBigEndian(data, 0, 0xB228C60A, 4); + PutBigEndian(data, 4, 1, 4); + PutBigEndian(data, 8, size, 8); + PutBigEndian(data, 0x10, files.size(), 4); + PutBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + PutBigEndian(data, entry + 0x20, offset, 8); + PutBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +static void Write(const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(file != nullptr, "a test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + Require(std::fclose(file) == 0 && written, "a test file was not written"); +} + +static void PostUnlock(int trophyId) { + static int context = 0; + static int handle = 0; + if (context == 0) { + Require(sceNpUniversalDataSystemCreateContext(&context, InitialUser, 0, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(context, handle, 0) == 0, "UDS context was not registered"); + } + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static int TrophyContext(std::uint32_t serviceLabel) { + int context = 0; + Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); + return context; +} + +static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned"); + return data; +} + +struct Unlocks { + std::vector trophies; +}; + +static void Remember(void* context, const NpTrophy2Unlock& unlock) { + Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.serviceLabel == 0 && unlock.tick != 0, "an unlock does not carry its title, user, label and time"); + static_cast(context)->trophies.push_back(unlock.trophyId); +} + +static void ReportTrophyData(int context) { + NpTrophy2GameDetails game{}; + const auto progress = Game(context, &game); + Require(std::strcmp(game.title, "Synthetic Game") == 0 && game.num_groups == 2 && game.num_trophies == 8 && game.num_platinum == 1 && game.num_gold == 1 && game.num_silver == 1 && game.num_bronze == 5, + "the game details are not the title's"); + Require(progress.unlocked_trophies == 0 && progress.progress_percentage == 0, "the game has unlocks before any event"); + + NpTrophy2Details details{}; + Trophy(context, 1, &details); + Require(std::strcmp(details.name, "Ten") == 0 && std::strcmp(details.description, "Reach ten") == 0 && details.trophy_grade == 2 && details.group_id == -1 && !details.hidden && + details.target.type == 0 && details.target.value == 0, + "trophy 1 does not report its name, grade and group"); + Trophy(context, 6, &details); + Require(std::strcmp(details.name, "Secret") == 0 && std::strcmp(details.reward, "A hat") == 0 && details.trophy_grade == 4 && details.group_id == 1 && details.hidden && details.has_reward, + "trophy 6 does not report its hidden flag, reward and group"); + Trophy(context, 0, &details); + Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported"); + const auto steps = Trophy(context, 5, &details); + Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target"); + NpTrophy2Data unknown{}; + Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported"); + + std::vector list(10); + std::vector listed(10); + std::uint32_t count = 0; + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, list.data(), listed.data(), &count) == 0 && count == 8, "the trophy list does not hold every trophy"); + for (std::uint32_t i = 0; i < count; ++i) Require(list[i].trophy_id == static_cast(i) && listed[i].trophy_id == static_cast(i), "the trophy list is not in id order"); + Require(std::strcmp(list[5].name, "Five steps") == 0 && listed[5].progress.type == 1, "the trophy list does not carry names and progress"); + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 6, 5, list.data(), nullptr, &count) == 0 && count == 2 && list[0].trophy_id == 6 && list[1].trophy_id == 7, + "the trophy list does not start at its offset"); + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 8, 5, nullptr, listed.data(), &count) == 0 && count == 0, "the trophy list goes past its end"); + + NpTrophy2GroupDetails group{}; + NpTrophy2GroupData groupData{}; + Require(sceNpTrophy2GetGroupInfo(context, 1, 1, &group, &groupData) == 0 && std::strcmp(group.title, "Expansion") == 0 && group.num_trophies == 2 && group.num_bronze == 2 && groupData.group_id == 1, + "group 1 is not reported"); + NpTrophy2GroupData unknownGroup{}; + Require(ThrowsInvalidArgument([context, &unknownGroup] { sceNpTrophy2GetGroupInfo(context, 1, 7, nullptr, &unknownGroup); }), "a group that is not in the package was reported"); + std::vector groups(4); + Require(sceNpTrophy2GetGroupInfoArray(context, 1, 0, 4, groups.data(), nullptr, &count) == 0 && count == 2 && groups[0].group_id == -1 && std::strcmp(groups[0].title, "Synthetic Game") == 0 && + groups[0].num_trophies == 6 && groups[1].group_id == 1, + "the group list does not hold the base game and group 1"); +} + +static void PostUnlocks(int context) { + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); + for (const int trophyId : {1, 2, 3, 6}) PostUnlock(trophyId); + const auto partway = Game(context); + Require(!Trophy(context, 0).unlocked && partway.unlocked_trophies == 4 && partway.unlocked_gold == 1 && partway.unlocked_silver == 1 && partway.unlocked_bronze == 2 && + partway.progress_percentage == (6 + 2 + 2) * 100 / (6 + 2 + 5), + "the game does not count the unlocks by grade"); + PostUnlock(4); + Require(!Trophy(context, 0).unlocked, "the platinum unlocked before every trophy that names it"); + for (const int trophyId : {5, 0, 42}) PostUnlock(trophyId); + const auto refused = Game(context); + Require(!Trophy(context, 5).unlocked && !Trophy(context, 0).unlocked && refused.unlocked_trophies == 5 && refused.unlocked_platinum == 0 && refused.unlocked_bronze == 3 && + refused.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), + "a progressive, platinum or unknown trophy posted through UDS unlocked, or changed the counts"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + Require(unlocks.trophies == (std::vector{1, 2, 3, 6, 4}), "the listener did not see each posted unlock once, in order, or saw a refused one"); +} + +static void ReportPlaceholders() { + for (const std::uint32_t serviceLabel : {1u, 2u}) { + const auto context = TrophyContext(serviceLabel); + NpTrophy2GameDetails game{}; + Game(context, &game); + NpTrophy2Details details{}; + Trophy(context, 12, &details); + std::uint32_t count = 0; + NpTrophy2Data listed{}; + Require(std::strcmp(game.title, "Game") == 0 && std::strcmp(details.name, "Trophy") == 0 && + sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, nullptr, &listed, &count) == 0 && count == 1, + "a missing or malformed trophy package does not leave the placeholders"); + } +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + Write("app0/sce_sys/trophy2/trophy00.ucp", Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}})); + Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package"); + + const auto context = TrophyContext(0); + ReportTrophyData(context); + PostUnlocks(context); + ReportPlaceholders(); + + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy data tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp new file mode 100644 index 0000000000..a8e94f7516 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -0,0 +1,365 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32(NpUniversalDataSystemEventPropertyObject*, const char*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64(NpUniversalDataSystemEventPropertyObject*, const char*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64(NpUniversalDataSystemEventPropertyObject*, const char*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32(NpUniversalDataSystemEventPropertyObject*, const char*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +} + +static constexpr int InitialUser = 0x10000000; + +static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}}, + {"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true}, + {"id":"7","grade":"B","groupId":"1"} +]})"; + +static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}], + "groupMetadata":[{"id":"1","name":"Expansion"}]}})"; + +static const char StatsDefinition[] = R"({"statDefinitionArray":[ + {"statId":10,"dataType":"uint64","aggregation":"latest"},{"statId":20,"dataType":"uint64","aggregation":"latest"}, + {"statId":30,"dataType":"uint32","aggregation":"latest"},{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"; + +static const char SumDefinition[] = R"({"statDefinitionArray":[{"statId":10,"dataType":"uint64","aggregation":"sum"}]})"; + +static const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, + {"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}}, + {"condition":{"eventName":"STEPS"},"action":{"input":"$.player.steps","output":{"statId":40}}}]})"; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +static std::string Ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + PutBigEndian(data, 0, 0xB228C60A, 4); + PutBigEndian(data, 4, 1, 4); + PutBigEndian(data, 8, size, 8); + PutBigEndian(data, 0x10, files.size(), 4); + PutBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + PutBigEndian(data, entry + 0x20, offset, 8); + PutBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +static void Write(const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(file != nullptr, "a test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + Require(std::fclose(file) == 0 && written, "a test file was not written"); +} + +static std::string Read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +static bool Exists(const char* path) { + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return false; + std::fclose(file); + return true; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + Require(saved >= 0 && _dup2(_fileno(file), _fileno(stderr)) == 0, "cannot capture stderr"); +#else + saved = dup(fileno(stderr)); + Require(saved >= 0 && dup2(fileno(file), fileno(stderr)) == fileno(stderr), "cannot capture stderr"); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +struct Uds { + int context = 0; + int handle = 0; +}; + +static Uds OpenUds(std::uint32_t serviceLabel) { + Uds uds; + Require(sceNpUniversalDataSystemCreateContext(&uds.context, InitialUser, serviceLabel, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&uds.handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(uds.context, uds.handle, 0) == 0, "UDS context was not registered"); + return uds; +} + +template +static void Post(const Uds& uds, const char* name, TSet set) { + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + set(properties); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent(name, properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(uds.context, uds.handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static int TrophyContext(std::uint32_t serviceLabel) { + int context = 0; + Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); + return context; +} + +static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned"); + return data; +} + +struct Unlocks { + std::vector> trophies; +}; + +static void Remember(void* context, const NpTrophy2Unlock& unlock) { + Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.tick != 0, "an unlock does not carry its title, user and time"); + static_cast(context)->trophies.emplace_back(unlock.serviceLabel, unlock.trophyId); +} + +static void KeepStatsAcrossRuns(int context) { + NpTrophy2Details details{}; + const auto steps = Trophy(context, 5, &details); + Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 2, + "the progressive trophy does not report the stat kept from the last run"); +} + +static void UnlockThroughStats(int context, Unlocks& unlocks) { + const auto uds = OpenUds(0); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "counter", 9) == 0, "counter was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "total", 100) == 0, "total was not set"); + }); + Require(!Trophy(context, 1).unlocked && !Trophy(context, 2).unlocked && unlocks.trophies.empty(), "ge or gt unlocked below its target"); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(Trophy(context, 1).unlocked && !Trophy(context, 2).unlocked, "ge did not unlock at its target"); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "counter", 3) == 0, "counter was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt64(properties, "total", 101) == 0, "total was not set"); + }); + Require(Trophy(context, 2).unlocked, "gt did not unlock past its target"); + + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 6) == 0, "seconds was not set"); + }); + Require(!Trophy(context, 3).unlocked && !Trophy(context, 4).unlocked, "le or lt unlocked above its target"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 5) == 0, "seconds was not set"); + }); + Require(Trophy(context, 3).unlocked && !Trophy(context, 4).unlocked, "le did not unlock at its target"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "seconds", -1) == 0, "seconds was not set"); + }); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetFloat32(properties, "seconds", 1.0f) == 0, "seconds was not set"); + }); + Require(!Trophy(context, 4).unlocked, "a negative or fractional value reached lt"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 2) == 0, "seconds was not set"); + }); + Require(Trophy(context, 4).unlocked, "lt did not unlock below its target"); + + Post(uds, "NOTHING", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "steps", 50) == 0, "steps was not set"); + }); + const auto stepped = [&uds](std::uint32_t steps) { + Post(uds, "STEPS", [steps](NpUniversalDataSystemEventPropertyObject* properties) { + NpUniversalDataSystemEventPropertyObject* player = nullptr; + Require(sceNpUniversalDataSystemEventPropertyObjectSetObject(properties, "player", nullptr, &player) == 0, "player was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(player, "steps", steps) == 0, "steps was not set"); + }); + }; + stepped(4); + const auto partway = Trophy(context, 5); + Require(!partway.unlocked && partway.progress.type == 1 && partway.progress.value == 4 && !Trophy(context, 0).unlocked, "the progressive trophy does not report its progress"); + stepped(5); + const auto done = Trophy(context, 5); + Require(done.unlocked && done.progress.value == 5, "the progressive trophy did not unlock at its target"); + Require(Trophy(context, 0).unlocked, "the platinum did not unlock with the last trophy that names it"); + + const std::vector> expected{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 0}}; + Require(unlocks.trophies == expected, "the listener did not see each stat unlock and the platinum once, in order"); + const auto game = Game(context); + Require(game.unlocked_trophies == 6 && game.unlocked_platinum == 1 && game.unlocked_gold == 1 && game.unlocked_silver == 1 && game.unlocked_bronze == 3 && + game.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), + "the game does not count the unlocks by grade"); + Require(Read("_trophy/268435456_0.stats.txt") == "10 3\n20 101\n30 2\n40 5\n", "the stats were not stored next to the unlocks"); + const auto record = Read("_trophy/268435456_0.txt"); + for (const char* line : {"0 ", "1 ", "2 ", "3 ", "4 ", "5 "}) Require(record.find(std::string("\n") + line) != std::string::npos || record.starts_with(line), "an unlock was not stored"); +} + +static void IgnoreMissingAndMalformedFiles(Unlocks& unlocks) { + const auto missing = TrophyContext(1); + NpTrophy2GameDetails game{}; + Game(missing, &game); + Require(std::strcmp(game.title, "Game") == 0, "a title without trophy files does not report placeholders"); + Post(OpenUds(1), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(!Exists("_trophy/268435456_1.stats.txt"), "a title without stat rules stored a stat"); + + const auto brokenTrophies = TrophyContext(2); + Game(brokenTrophies, &game); + Require(std::strcmp(game.title, "Game") == 0, "a malformed trophy package was used"); + Post(OpenUds(2), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(Read("_trophy/268435456_2.stats.txt") == "10 10\n" && !Exists("_trophy/268435456_2.txt"), "a malformed trophy package disabled the stat rules, or stats unlocked a trophy without one"); + + const auto brokenRules = TrophyContext(3); + Game(brokenRules, &game); + Require(std::strcmp(game.title, "Synthetic Game") == 0, "malformed stat rules disabled the trophy package"); + Post(OpenUds(3), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(!Exists("_trophy/268435456_3.stats.txt") && !Trophy(brokenRules, 1).unlocked, "malformed stat rules were used"); + Require(unlocks.trophies.size() == 6, "a missing or malformed file unlocked a trophy"); +} + +static void IgnoreOtherAggregations(Unlocks& unlocks) { + StderrCapture capture; + const auto context = TrophyContext(5); + Post(OpenUds(5), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + const auto log = capture.Finish(); + Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" is supported") != std::string::npos, + "a stat with a sum aggregation was not reported"); + Require(!Trophy(context, 1).unlocked && !Exists("_trophy/268435456_5.stats.txt") && unlocks.trophies.size() == 6, "a stat with a sum aggregation was stored or unlocked a trophy"); +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-stats-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + const auto trophies = Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}}); + const auto rules = Ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", StatsExtraction}}); + Write("app0/sce_sys/trophy2/trophy00.ucp", trophies); + Write("app0/sce_sys/uds/uds00.ucp", rules); + Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package"); + Write("app0/sce_sys/uds/uds02.ucp", rules); + Write("app0/sce_sys/trophy2/trophy03.ucp", trophies); + Write("app0/sce_sys/uds/uds03.ucp", rules.substr(0, rules.size() - 1)); + Write("app0/sce_sys/trophy2/trophy05.ucp", trophies); + Write("app0/sce_sys/uds/uds05.ucp", Ucp({{"stats_definition.json", SumDefinition}, {"stats_extraction.json", StatsExtraction}})); + Write("_trophy/268435456_0.stats.txt", "40 2\n"); + + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); + const auto context = TrophyContext(0); + KeepStatsAcrossRuns(context); + UnlockThroughStats(context, unlocks); + IgnoreMissingAndMalformedFiles(unlocks); + IgnoreOtherAggregations(unlocks); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy stat tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpTrophyUnlocks.cpp b/core/libs/tests/GuestNpTrophyUnlocks.cpp new file mode 100644 index 0000000000..be3d6e2859 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyUnlocks.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" + +#include +#include +#include +#include +#include +#include +#include + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +} + +static constexpr int InitialUser = 0x10000000; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static std::string Read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +static void Post(int context, int handle, const char* name, int trophyId) { + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent(name, properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static NpTrophy2Data Trophy(int context, int handle, int trophyId) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, handle, trophyId, nullptr, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static std::uint32_t Unlocked(int context, int handle) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, handle, nullptr, &data) == 0, "game info was not returned"); + return data.unlocked_trophies; +} + +static void UnlockThroughEvents() { + std::filesystem::create_directories("app0/sce_sys"); + std::FILE* param = std::fopen("app0/sce_sys/param.json", "wb"); + Require(param != nullptr, "param.json was not created"); + std::fputs(R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})", param); + std::fclose(param); + int udsContext = 0; + int handle = 0; + Require(sceNpUniversalDataSystemCreateContext(&udsContext, InitialUser, 0, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(udsContext, handle, 0) == 0, "UDS context was not registered"); + int trophyContext = 0; + Require(sceNpTrophy2CreateContext(&trophyContext, InitialUser, 0, 0) == 0, "trophy context was not created"); + + Require(!Trophy(trophyContext, handle, 12).unlocked && Unlocked(trophyContext, handle) == 0, "trophy 12 is unlocked before its event"); + Post(udsContext, handle, "_UnlockTrophy", 12); + const auto unlocked = Trophy(trophyContext, handle, 12); + Require(unlocked.unlocked && unlocked.timestamp_tick != 0, "the posted unlock is not reported with its time"); + Require(Unlocked(trophyContext, handle) == 1, "the game does not count the posted unlock"); + + Post(udsContext, handle, "_ChallengeComplete", 13); + Require(!Trophy(trophyContext, handle, 13).unlocked, "an unknown event unlocked a trophy"); + Post(udsContext, handle, "_UnlockTrophy", 12); + Require(Trophy(trophyContext, handle, 12).timestamp_tick == unlocked.timestamp_tick && Unlocked(trophyContext, handle) == 1, "a repeated unlock changed the record"); + + Require(Read("_trophy/268435456_0.txt") == "12 " + std::to_string(unlocked.timestamp_tick) + "\n", "the unlock was not stored in the title folder"); +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-unlocks-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + UnlockThroughEvents(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + return 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/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp new file mode 100644 index 0000000000..639a8297fe --- /dev/null +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -0,0 +1,61 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePlayerSelectionDialogInitialize(void); +int APS5_VABI scePlayerSelectionDialogOpen(const void* param); +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); +int APS5_VABI scePlayerSelectionDialogGetStatus(void); +int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI scePlayerSelectionDialogClose(void); +int APS5_VABI scePlayerSelectionDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogUpdateStatus() == 0); + + Require(scePlayerSelectionDialogInitialize() == 0); + Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized); + Require(scePlayerSelectionDialogGetStatus() == 1); + Require(scePlayerSelectionDialogUpdateStatus() == 1); + + Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(result) == kErrNotFinished); + + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); + + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(scePlayerSelectionDialogClose() == 0); + Require(scePlayerSelectionDialogTerminate() == 0); + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); +} diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 1738f11dd8..131e75eb9d 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include struct PollDescriptor { int descriptor; @@ -31,8 +32,12 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI unlink_nid_postfix(const char*); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI kqueue_nid_postfix(); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); int APS5_VABI fcntl_nid_postfix(int, int, ...); @@ -55,6 +60,33 @@ constexpr short Out = 0x4; constexpr short Hup = 0x10; constexpr short Invalid = 0x20; constexpr short ReadNormal = 0x40; +constexpr short WriteBand = 0x100; +static void PollsFilesAndDirectories(int socket) { + constexpr int ReadWrite = 0x0002, Create = 0x0200, DirectoryFlag = 0x00020000; + const auto stamp = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::string filePath = "anyps5-poll-file-" + stamp; + const std::string directoryPath = "anyps5-poll-directory-" + stamp; + const int file = open_nid_postfix(filePath.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + Require(mkdir_nid_postfix(directoryPath.c_str(), 0755) == 0); + const int directory = open_nid_postfix(directoryPath.c_str(), DirectoryFlag, 0); + Require(directory >= 0); + PollDescriptor vnode{file, static_cast(In | Out | ReadNormal | Priority | ReadBand | WriteBand), -1}; + Require(poll_nid_postfix(&vnode, 1, 0) == 1 && vnode.revents == (In | Out | ReadNormal)); + vnode.events = static_cast(Priority | ReadBand | WriteBand); + Require(poll_nid_postfix(&vnode, 1, 0) == 0 && vnode.revents == 0); + const auto start = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&vnode, 1, 30) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(25)); + PollDescriptor folder{directory, static_cast(In | Out), -1}; + Require(poll_nid_postfix(&folder, 1, -1) == 1 && folder.revents == (In | Out)); + PollDescriptor mixed[3]{{socket, In, -1}, {file, Out, -1}, {directory, ReadNormal, -1}}; + Require(poll_nid_postfix(mixed, 3, -1) == 2); + Require(mixed[0].revents == 0 && mixed[1].revents == Out && mixed[2].revents == ReadNormal); + Require(close_nid_postfix(directory) == 0 && close_nid_postfix(file) == 0); + Require(rmdir_nid_postfix(directoryPath.c_str()) == 0 && unlink_nid_postfix(filePath.c_str()) == 0); +} + static void TestPipes(int socket) { int ends[2]; Require(pipe_nid_postfix(ends) == 0); @@ -64,6 +96,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}; @@ -217,17 +253,38 @@ int main() { Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22); TestPipes(listener); + PollsFilesAndDirectories(listener); Require(close_nid_postfix(server) == 0); - const int file = open_nid_postfix("/dev/urandom", 0, 0); - Require(file >= 0); - PollDescriptor mixed[2]{{listener, In, 0}, {file, In, 0}}; + for (const char* path : {"/dev/urandom", "/dev/random"}) { + const int random = open_nid_postfix(path, 0, 0); + Require(random >= 0); + PollDescriptor device{random, static_cast(In | Out | Priority), -1}; + Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == In); + device.events = static_cast(ReadNormal | Out); + Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == ReadNormal); + device.events = static_cast(Out | Priority | ReadBand | WriteBand); + Require(poll_nid_postfix(&device, 1, -1) == 1 && device.revents == (Out | Priority | ReadBand | WriteBand)); + PollDescriptor mixed[2]{{listener, In, 0}, {random, Out, 0}}; + Require(poll_nid_postfix(mixed, 2, -1) == 1 && mixed[0].revents == 0 && mixed[1].revents == Out); + Require(close_nid_postfix(random) == 0); + } + PollDescriptor console[3]{{0, static_cast(In | ReadNormal | Out), -1}, {1, static_cast(In | Out | WriteBand), -1}, {2, Out, -1}}; + Require(poll_nid_postfix(console, 3, 0) == 2); + Require(console[0].revents == 0 && console[1].revents == Out && console[2].revents == Out); + PollDescriptor input{0, In, -1}; + const auto inputStart = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&input, 1, 30) == 0 && input.revents == 0); + Require(std::chrono::steady_clock::now() - inputStart >= std::chrono::milliseconds(25)); + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + PollDescriptor unknown[2]{{listener, In, 0}, {queue, In, 0}}; bool unsupported = false; try { - poll_nid_postfix(mixed, 2, 0); + poll_nid_postfix(unknown, 2, 0); } catch (const std::runtime_error&) { unsupported = true; } Require(unsupported); - Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(queue) == 0); Require(close_nid_postfix(listener) == 0); } 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/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index 8fe49f711d..33ae4ebd0f 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -12,6 +12,16 @@ int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes**); int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes**); int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes**, std::int16_t); int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); +int APS5_VABI posix_spawnattr_getflags_nid_postfix(PosixSpawnAttributes* const*, std::int16_t*); +int APS5_VABI posix_spawnattr_getpgroup_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getschedparam_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getschedpolicy_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getsigdefault_nid_postfix(PosixSpawnAttributes* const*, GuestSignalSet*); +int APS5_VABI posix_spawnattr_getsigmask_nid_postfix(PosixSpawnAttributes* const*, GuestSignalSet*); +int APS5_VABI posix_spawnattr_setpgroup_nid_postfix(PosixSpawnAttributes**, std::int32_t); +int APS5_VABI posix_spawnattr_setschedparam_nid_postfix(PosixSpawnAttributes**, const std::int32_t*); +int APS5_VABI posix_spawnattr_setschedpolicy_nid_postfix(PosixSpawnAttributes**, std::int32_t); +int APS5_VABI posix_spawnattr_setsigmask_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions**); int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions**); int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions**, int, const char*, int, std::uint16_t); @@ -67,7 +77,30 @@ static void CheckAttributes() { Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); Check(std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + std::int16_t flags = 1; + std::int32_t value = 1; + GuestSignalSet read{{9u, 9u, 9u, 9u}}; + Check(posix_spawnattr_getflags_nid_postfix(&attributes, &flags) == 0 && flags == 0); + Check(posix_spawnattr_getsigdefault_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &defaults, sizeof(read)) == 0); + Check(posix_spawnattr_getsigmask_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &empty, sizeof(read)) == 0); + Check(posix_spawnattr_setpgroup_nid_postfix(&attributes, 77) == 0 && attributes->processGroup == 77); + Check(posix_spawnattr_getpgroup_nid_postfix(&attributes, &value) == 0 && value == 77); + const std::int32_t priority = 450; + Check(posix_spawnattr_setschedparam_nid_postfix(&attributes, &priority) == 0 && attributes->schedulingPriority == 450); + Check(posix_spawnattr_getschedparam_nid_postfix(&attributes, &value) == 0 && value == 450); + Check(posix_spawnattr_setschedpolicy_nid_postfix(&attributes, 2) == 0 && attributes->schedulingPolicy == 2); + Check(posix_spawnattr_getschedpolicy_nid_postfix(&attributes, &value) == 0 && value == 2); + const GuestSignalSet mask{{1u, 2u, 3u, 4u}}; + Check(posix_spawnattr_setsigmask_nid_postfix(&attributes, &mask) == 0 && std::memcmp(&attributes->signalMask, &mask, sizeof(mask)) == 0); + Check(posix_spawnattr_getsigmask_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &mask, sizeof(read)) == 0); + Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); + PosixSpawnAttributes* uninitialized = nullptr; + Check(ThrowsInvalidArgument([&] { posix_spawnattr_getflags_nid_postfix(&uninitialized, &flags); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_getpgroup_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setpgroup_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setschedparam_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigmask_nid_postfix(&attributes, nullptr); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setflags_nid_postfix(&uninitialized, 1); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&uninitialized, &defaults); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&attributes, nullptr); })); diff --git a/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp index e5a76a3bb8..27f0e6dff8 100644 --- a/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp +++ b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp @@ -9,6 +9,7 @@ int APS5_VABI sceProprietaryVoiceChatHelperInitialize(); int APS5_VABI sceProprietaryVoiceChatHelperTerminate(); int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(); int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChat(); } static void Require(bool value, const char* what) { @@ -32,6 +33,8 @@ int main() { "setting the state before initialization did not throw"); Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), "reading the usage state before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "getting voice chat before initialization did not throw"); Require(Throws([] { sceProprietaryVoiceChatHelperTerminate(); }), "terminating before initialization did not throw"); @@ -41,10 +44,14 @@ int main() { Require(sceProprietaryVoiceChatHelperSetVoiceChatState() == 0, "setting the state failed"); Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), "reading the unknown usage state did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "calling unknown voice chat export did not throw"); Require(sceProprietaryVoiceChatHelperTerminate() == 0, "termination failed"); Require(Throws([] { sceProprietaryVoiceChatHelperSetVoiceChatState(); }), "setting the state after termination did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "getting voice chat after termination did not throw"); Require(sceProprietaryVoiceChatHelperInitialize() == 0, "initialization after termination failed"); Require(sceProprietaryVoiceChatHelperTerminate() == 0, "termination after reinitialization failed"); 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/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index 6686ebc013..6fbdcb4bad 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -73,12 +73,13 @@ int main() { Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, 2) == GUEST_EINVAL); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, -1) == GUEST_EINVAL); - Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == 0); - Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_SHARED); - Context shared; - Require(pthread_barrier_init_nid_postfix(&shared.barrier, &attr, WAITERS) == 0); - RunWaiters(shared); - Require(pthread_barrier_destroy_nid_postfix(&shared.barrier) == 0); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == GUEST_EINVAL); + pshared = -1; + Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Context afterRejectedShared; + Require(pthread_barrier_init_nid_postfix(&afterRejectedShared.barrier, &attr, WAITERS) == 0); + RunWaiters(afterRejectedShared); + Require(pthread_barrier_destroy_nid_postfix(&afterRejectedShared.barrier) == 0); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_PRIVATE) == 0); pshared = -1; Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); 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..237ac4374e --- /dev/null +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -0,0 +1,62 @@ +#include "SceTypes.hpp" +#include +#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); +int APS5_VABI pthread_condattr_getpshared_nid_postfix(const 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; + int sharing = -1; + 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); + Require(pthread_condattr_getpshared_nid_postfix(nullptr, &sharing) == GuestEinval && sharing == -1); + Require(pthread_condattr_getpshared_nid_postfix(&unset, &sharing) == GuestEinval && sharing == -1); + + 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); + } + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + for (const int invalid : {1, 2, -1}) { + Require(pthread_condattr_setpshared_nid_postfix(&attr, invalid) == GuestEinval); + sharing = -1; + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + } + Require(pthread_condattr_setpshared_nid_postfix(&attr, 0) == 0); + sharing = -1; + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + bool threw = false; + try { + pthread_condattr_getpshared_nid_postfix(&attr, nullptr); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); + Require(scePthreadCondattrDestroy(&attr) == SceEinval); + sharing = -1; + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == GuestEinval && sharing == -1); + Require(pthread_condattr_setpshared_nid_postfix(&attr, 0) == GuestEinval); +} 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/GuestPthreadJoinSelf.cpp b/core/libs/tests/GuestPthreadJoinSelf.cpp index 742fa9d734..2fec26bef7 100644 --- a/core/libs/tests/GuestPthreadJoinSelf.cpp +++ b/core/libs/tests/GuestPthreadJoinSelf.cpp @@ -1,12 +1,16 @@ #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); Pthread APS5_VABI scePthreadSelf(void); int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); +int APS5_VABI scePthreadDetach(Pthread thread); } static constexpr int SCE_OK = 0; @@ -23,6 +27,18 @@ static void* APS5_VABI JoinSelf(void*) { return reinterpret_cast(std::uintptr_t{0x1234}); } +static std::atomic detachedChecks{0}; + +static void* APS5_VABI DetachThenJoinSelf(void*) { + void* result = reinterpret_cast(std::uintptr_t{0x66}); + Require(scePthreadDetach(scePthreadSelf()) == SCE_OK); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast(std::uintptr_t{0x66})); + detachedChecks.store(1); + return nullptr; +} + int main() { Pthread thread = nullptr; void* result = nullptr; @@ -36,4 +52,14 @@ int main() { Require(scePthreadCreate(&thread, &unset, JoinSelf, nullptr, "unset-attr") == SCE_OK); Require(scePthreadJoin(thread, &result) == SCE_OK); Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + + thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, DetachThenJoinSelf, nullptr, "detach-join-self") == SCE_OK); + for (int wait = 0; wait < 5000 && detachedChecks.load() == 0; ++wait) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(detachedChecks.load() == 1); + + result = reinterpret_cast(std::uintptr_t{0x77}); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast(std::uintptr_t{0x77})); } diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index a201ff6d92..7e704677e5 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -90,11 +90,10 @@ static void CheckProcessSharing() { 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}) + for (const int invalid : {1, -1, 2, 2147483647}) Require(pthread_mutexattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); - Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); + sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); int type = -1; Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 2); Require(Throws([&] { pthread_mutexattr_getpshared_nid_postfix(&attr, nullptr); })); @@ -113,11 +112,22 @@ static void CheckProcessSharing() { 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); @@ -139,7 +149,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..b7f54df37e 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -14,6 +14,8 @@ int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_init_nid_postfix(PthreadRwlock* rwlock, const PthreadRwlockattr* attr); int APS5_VABI pthread_rwlockattr_init_nid_postfix(PthreadRwlockattr* attr); int APS5_VABI pthread_rwlockattr_destroy_nid_postfix(PthreadRwlockattr* attr); +int APS5_VABI pthread_rwlockattr_getpshared_nid_postfix(const PthreadRwlockattr* attr, int* pshared); +int APS5_VABI pthread_rwlockattr_setpshared_nid_postfix(PthreadRwlockattr* attr, int pshared); int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock); @@ -39,6 +41,7 @@ static constexpr int GUEST_EBUSY = 16; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ETIMEDOUT = 60; static constexpr int GUEST_REALTIME_CLOCK = 0; +static constexpr int PROCESS_PRIVATE = 0; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000; static void Require(bool value) { if (!value) std::abort(); } @@ -309,7 +312,45 @@ static void CheckWaitingWriterHoldsBackNewReaders() { Require(pthread_rwlock_destroy_nid_postfix(&recursive) == 0); } +template +static bool Throws(Call call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void CheckAttrProcessSharing() { + PthreadRwlockattr attr = nullptr; + Require(pthread_rwlockattr_init_nid_postfix(&attr) == 0); + int pshared = -1; + Require(pthread_rwlockattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + for (const int invalid : {1, 2, -1}) { + Require(pthread_rwlockattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); + pshared = -1; + Require(pthread_rwlockattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + } + Require(pthread_rwlockattr_setpshared_nid_postfix(&attr, PROCESS_PRIVATE) == 0); + pshared = -1; + Require(pthread_rwlockattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Require(Throws([&] { pthread_rwlockattr_getpshared_nid_postfix(&attr, nullptr); })); + + PthreadRwlockattr unset = nullptr; + for (const int invalid : {1, 2, -1}) { + Require(pthread_rwlockattr_setpshared_nid_postfix(nullptr, invalid) == GUEST_EINVAL); + Require(pthread_rwlockattr_setpshared_nid_postfix(&unset, invalid) == GUEST_EINVAL); + } + Require(Throws([&] { pthread_rwlockattr_setpshared_nid_postfix(nullptr, PROCESS_PRIVATE); })); + Require(Throws([&] { pthread_rwlockattr_setpshared_nid_postfix(&unset, PROCESS_PRIVATE); })); + Require(Throws([&] { pthread_rwlockattr_getpshared_nid_postfix(nullptr, &pshared); })); + Require(Throws([&] { pthread_rwlockattr_getpshared_nid_postfix(&unset, &pshared); })); + Require(pthread_rwlockattr_destroy_nid_postfix(&attr) == 0); +} + int main() { + CheckAttrProcessSharing(); CheckWaitingWriterHoldsBackNewReaders(); CheckConcurrentFirstWrlockExcludes(); CheckDeadlinesBeyondTheClockRange(); @@ -379,10 +420,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/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp index 9783df2dd7..ca7e2b6926 100644 --- a/core/libs/tests/GuestPthreadSpin.cpp +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -51,6 +51,7 @@ static void* APS5_VABI Incrementer(void* arg) { static void ArgumentErrors() { Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, PTHREAD_PROCESS_SHARED) == GUEST_EINVAL); Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); Require(lock == nullptr); @@ -116,7 +117,6 @@ static void MutualExclusion() { int main() { ArgumentErrors(); SingleThread(PTHREAD_PROCESS_PRIVATE); - SingleThread(PTHREAD_PROCESS_SHARED); HeldByAnotherThread(); MutualExclusion(); } 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/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 4a75f5c01e..6124f2540f 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -1,15 +1,58 @@ +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include #include #include #include #include extern "C" { +int APS5_VABI mergesort_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*)); +int* APS5_VABI __error_nid_postfix(); void APS5_VABI qsort_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*)); } namespace { +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()); +} + struct Element { int key; int tag; @@ -66,6 +109,31 @@ bool ExpectSorted(std::size_t size, std::size_t count) { return true; } + +bool CheckMergesort() { + InitializeHeap(); + bool correct = true; + for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { + std::vector elements; + for (std::size_t index = 0; index < count; ++index) elements.push_back({static_cast((index * 7919) % 5), static_cast(index)}); + correct &= mergesort_nid_postfix(elements.data(), count, sizeof(Element), CompareKeys) == 0; + for (std::size_t index = 1; index < count; ++index) { + const auto& before = elements[index - 1]; + const auto& after = elements[index]; + correct &= before.key < after.key || (before.key == after.key && before.tag < after.tag); + } + } + unsigned char bytes[3] = {3, 1, 2}; + *__error_nid_postfix() = 0; + correct &= mergesort_nid_postfix(bytes, 3, 1, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; + correct &= mergesort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; + Element pair[2] = {{2, 0}, {1, 1}}; + failAllocation = true; + correct &= mergesort_nid_postfix(pair, 2, sizeof(Element), CompareKeys) == -1 && pair[0].key == 2; + failAllocation = false; + if (!correct) std::fprintf(stderr, "mergesort failed\n"); + return correct; +} } int main() { @@ -80,5 +148,6 @@ int main() { for (const std::size_t size : {1, 3, 8, 24}) for (const std::size_t count : {0, 1, 2, 6, 7, 8, 40, 41, 1000}) correct &= ExpectSorted(size, count); + correct &= CheckMergesort(); return correct ? 0 : 1; } 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/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 59b336db38..6f2ab74c7d 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -23,6 +23,8 @@ double APS5_VABI drand48_nid_postfix(); std::int64_t APS5_VABI lrand48_nid_postfix(); std::int64_t APS5_VABI mrand48_nid_postfix(); void APS5_VABI srand48_nid_postfix(std::int64_t); +std::uint16_t* APS5_VABI seed48_nid_postfix(const std::uint16_t*); +void APS5_VABI lcong48_nid_postfix(const std::uint16_t*); } static int failures = 0; @@ -74,6 +76,23 @@ static void CheckRand48() { Require(mrand48_nid_postfix() == jrand48_nid_postfix(expectedSeed), "mrand48 matches jrand48 on initialized seed"); Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 fourth value matches erand48"); + const std::uint16_t parameters[7] = {1, 0, 0, 3, 0, 0, 5}; + lcong48_nid_postfix(parameters); + Require(lrand48_nid_postfix() == 0 && mrand48_nid_postfix() == 0, "lcong48 sets seed, multiplier and addend"); + std::uint16_t high[3] = {0, 0, 1}; + Require(jrand48_nid_postfix(high) == 196608 && SeedIs(high, 5, 0, 3), "lcong48 multiplier scales the seed"); + std::uint16_t custom[3] = {2, 0, 0}; + Require(nrand48_nid_postfix(custom) == 0 && SeedIs(custom, 11, 0, 0), "lcong48 parameters apply to nrand48"); + const std::uint16_t newSeed[3] = {0x330e, 0xabcd, 0x1234}; + std::uint16_t* const echoed = seed48_nid_postfix(newSeed); + Require(SeedIs(echoed, 0x330e, 0xabcd, 0x1234), "seed48 returns a copy of the new seed"); + std::uint16_t again[3] = {0x330e, 0xabcd, 0x1234}; + Require(drand48_nid_postfix() == erand48_nid_postfix(again), "seed48 restores the default multiplier and addend"); + lcong48_nid_postfix(parameters); + srand48_nid_postfix(0x1234abcd); + std::uint16_t reset[3] = {0x330e, 0xabcd, 0x1234}; + Require(lrand48_nid_postfix() == nrand48_nid_postfix(reset), "srand48 restores the default multiplier and addend"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; 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/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 abdbe40aef..5e7786a5dd 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -12,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 { @@ -20,6 +22,7 @@ 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; @@ -31,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); @@ -42,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"); } @@ -106,6 +144,7 @@ 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"); 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..979415e15d 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -1,8 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include #include #include +#include +#include struct GuestTimeval { std::int64_t seconds; @@ -33,6 +36,10 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI kqueue_nid_postfix(); +int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int, const KernelTimespec*); int* APS5_VABI __error_nid_postfix(); } @@ -41,8 +48,62 @@ static void Require(bool value) { if (!value) std::abort(); } constexpr int GuestEbadf = 9; constexpr int GuestEinval = 22; constexpr int GuestDescriptorLimit = 1024; +constexpr std::int16_t EvfiltUser = -11; +constexpr std::uint16_t EvAdd = 0x0001; +constexpr std::uint16_t EvClear = 0x0020; +constexpr std::uint32_t NoteTrigger = 0x01000000; + +static int SelectAll(int descriptor, GuestDescriptorSet& read, GuestDescriptorSet& write, GuestDescriptorSet& except, const GuestTimeval& limit) { + read = {}; + write = {}; + except = {}; + read.Add(descriptor); + write.Add(descriptor); + except.Add(descriptor); + return select_nid_postfix(descriptor + 1, read.words.data(), write.words.data(), except.words.data(), &limit); +} + +static KernelEvent UserEvent(std::uint16_t flags, std::uint32_t fflags) { + KernelEvent change{}; + change.ident = 7; + change.filter = EvfiltUser; + change.flags = flags; + change.fflags = fflags; + return change; +} + +static void Kqueues() { + const int kq = kqueue_nid_postfix(); + Require(kq >= 0 && kq < GuestDescriptorLimit); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + GuestDescriptorSet except{}; + const GuestTimeval none{0, 0}; + Require(SelectAll(kq, read, write, except, none) == 0); + Require(!read.Has(kq) && !write.Has(kq) && !except.Has(kq)); + const auto added = UserEvent(EvAdd | EvClear, 0); + Require(kevent_nid_postfix(kq, &added, 1, nullptr, 0, nullptr) == 0); + Require(SelectAll(kq, read, write, except, none) == 0); + std::thread trigger([kq] { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const auto triggered = UserEvent(0, NoteTrigger); + Require(kevent_nid_postfix(kq, &triggered, 1, nullptr, 0, nullptr) == 0); + }); + const GuestTimeval second{1, 0}; + Require(SelectAll(kq, read, write, except, second) == 1); + trigger.join(); + Require(read.Has(kq) && !write.Has(kq) && !except.Has(kq)); + Require(SelectAll(kq, read, write, except, none) == 1 && read.Has(kq)); + KernelEvent events[1]{}; + const KernelTimespec zero{0, 0}; + Require(kevent_nid_postfix(kq, nullptr, 0, events, 1, &zero) == 1); + Require(SelectAll(kq, read, write, except, none) == 0); + Require(close_nid_postfix(kq) == 0); +} int main() { + Kqueues(); + int descriptors[2]{-1, -1}; Require(pipe_nid_postfix(descriptors) == 0); const int reader = descriptors[0]; @@ -59,6 +120,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); @@ -75,6 +149,38 @@ int main() { Require(buffer[0] == byte); } + { + const std::string path = "anyps5-select-file-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0 && random >= 0); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + GuestDescriptorSet except{}; + for (const int descriptor : {file, random}) { + read.Add(descriptor); + write.Add(descriptor); + except.Add(descriptor); + } + read.Add(reader); + const int highest = file > random ? (file > reader ? file : reader) : (random > reader ? random : reader); + const GuestTimeval zero{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), except.words.data(), &zero) == 3); + Require(read.Has(file) && write.Has(file) && !except.Has(file)); + Require(read.Has(random) && !write.Has(random) && !except.Has(random)); + GuestDescriptorSet randomWrite{}; + GuestDescriptorSet randomExcept{}; + randomWrite.Add(random); + randomExcept.Add(random); + Require(select_nid_postfix(random + 1, nullptr, randomWrite.words.data(), randomExcept.words.data(), &zero) == 2); + Require(randomWrite.Has(random) && randomExcept.Has(random)); + Require(!read.Has(reader)); + GuestDescriptorSet onlyExcept{}; + onlyExcept.Add(file); + Require(select_nid_postfix(file + 1, nullptr, nullptr, onlyExcept.words.data(), &zero) == 0 && !onlyExcept.Has(file)); + Require(close_nid_postfix(random) == 0 && close_nid_postfix(file) == 0 && unlink_nid_postfix(path.c_str()) == 0); + } + { GuestDescriptorSet write{}; write.Add(writer); diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index ddbea3ad02..786ddae94b 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -19,6 +19,7 @@ int APS5_VABI sigdelset_nid_postfix(GuestSignalSet*, int); int APS5_VABI sigismember_nid_postfix(const GuestSignalSet*, int); int* APS5_VABI __error_nid_postfix(); int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); [[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); [[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); int APS5_VABI JumpKeepingCalleeSaved(std::uint64_t* buffer); @@ -164,9 +165,28 @@ static void CheckSigsetjmp() { SetBlockedMask(0); } +static void CheckInvalidHow() { + SetBlockedMask(0x5u); + const GuestSignalSet blocked{{0x30u, 0, 0, 0}}; + for (const int how : {0, 4, -1, 1000}) { + GuestSignalSet previous{{0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u}}; + *__error_nid_postfix() = 0; + Require(_sigprocmask_nid_postfix(how, &blocked, &previous) == -1 && *__error_nid_postfix() == 22); + Require(Equals(previous, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u)); + *__error_nid_postfix() = 0; + Require(sigprocmask_nid_postfix(how, &blocked, &previous) == -1 && *__error_nid_postfix() == 22); + Require(Equals(previous, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u)); + Require(BlockedMask() == 0x5u); + Require(_sigprocmask_nid_postfix(how, nullptr, &previous) == 0); + Require(Equals(previous, 0x5u, 0, 0, 0)); + } + SetBlockedMask(0); +} + int main() { CheckSigsetjmp(); CheckSiglongjmp(); + CheckInvalidHow(); static constexpr int Untouched = 5; GuestSignalSet set{{0x12345678u, 0x9abcdef0u, 0xffffffffu, 1u}}; *__error_nid_postfix() = Untouched; 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..632f528ccf 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -156,7 +156,7 @@ static void CloseReleasesNumber() { unsigned char bytes[4]{}; Require(read_nid_postfix(socket, bytes, sizeof(bytes)) == sizeof(bytes)); PollDescriptor entry{socket, In, 0}; - Require(Throws([&] { poll_nid_postfix(&entry, 1, 0); })); + Require(poll_nid_postfix(&entry, 1, 0) == 1 && entry.revents == In); for (const int file : files) Require(close_nid_postfix(file) == 0); const int kernelClosed = Socket(); Require(sceKernelClose(kernelClosed) == 0); @@ -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/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 06e23b986e..982eb625d7 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -243,6 +243,51 @@ int main(int argc, char** argv) { if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); } + if (api == 5) { + for (const bool readOnlyAddress : {true, false}) { + auto* address = static_cast(outputMapping) + 16; + auto* length = reinterpret_cast(static_cast(outputMapping) + page + 16); + { + const GuestArena::HostWrite addressDestination(address, 32); + const GuestArena::HostWrite lengthDestination(length, sizeof(*length)); + Require(addressDestination.Open() && lengthDestination.Open(), "initialize refused sender outputs"); + std::memset(address, 0xa5, 32); + *length = 32; + } + void* refused = static_cast(outputMapping) + (readOnlyAddress ? 0 : page); + Require(sceKernelMprotect(refused, page, 1) == 0, "make one sender output read-only"); + send(); + waitForPacket(); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, address, length) == static_cast(0x8041010eu) && *error == 14, + "read-only sender output fails with EFAULT"); + Require(*length == 32 && std::all_of(address, address + 32, [](unsigned char value) { return value == 0xa5; }), + "refused sender outputs remain unchanged"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "refused sender output preserves queued data"); + Require(sceKernelMprotect(refused, page, 3) == 0, "restore sender output permission"); + } + auto* address = static_cast(outputMapping) + 16; + Require(sceKernelMprotect(outputMapping, page, 1) == 0, "make an unused sender address read-only"); + std::uint32_t zero = 0; + std::array recovery{}; + send(); + waitForPacket(); + Require(receive(recovery.data(), recovery.size(), 0, address, &zero) == sizeof(payload) && zero == 0 && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "zero-capacity sender address needs no write access"); + Require(sceKernelMprotect(outputMapping, page, 3) == 0, "restore unused sender address permission"); + address = static_cast(outputMapping) + page - senderAddress.size(); + Require(sceKernelMprotect(static_cast(outputMapping) + page, page, 1) == 0, + "make bytes beyond the sender address read-only"); + std::uint32_t oversized = 32; + send(); + waitForPacket(); + Require(receive(recovery.data(), recovery.size(), 0, address, &oversized) == sizeof(payload) && + oversized == senderAddress.size() && std::memcmp(address, senderAddress.data(), oversized) == 0, + "guard only the sender address bytes the receive writes"); + Require(sceKernelMprotect(static_cast(outputMapping) + page, page, 3) == 0, + "restore bytes beyond the sender address"); + } Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping"); Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); 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..4cb3a27c3e 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -23,6 +23,16 @@ 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*); +int APS5_VABI sceSslClose(int); +int APS5_VABI sceSslGetNotBefore(int, void*, SceSsl::RtcTick*); +int APS5_VABI sceSslGetNotAfter(int, void*, SceSsl::RtcTick*); +int APS5_VABI sceSslGetFingerprint(int, void*, int, unsigned char*); } namespace { @@ -157,6 +167,148 @@ 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); }); + + SceSsl::RtcTick begin{}; + Check(sceSslGetNotBefore(id, cert, &begin) == 0, "get not before failed"); + Check(begin.tick == 63927094442000000ULL, "not before tick mismatch"); + Check(sceSslGetNotBefore(id, cert, nullptr) == static_cast(0x8095177a), "null begin accepted"); + Throws([&] { sceSslGetNotBefore(id, nullptr, &begin); }); + Throws([&] { sceSslGetNotBefore(id, fake, &begin); }); + Throws([&] { sceSslGetNotBefore(9999, cert, &begin); }); + + SceSsl::RtcTick limit{}; + Check(sceSslGetNotAfter(id, cert, &limit) == 0, "get not after failed"); + Check(limit.tick == 64242454442000000ULL, "not after tick mismatch"); + Check(sceSslGetNotAfter(id, cert, nullptr) == static_cast(0x8095177a), "null limit accepted"); + Throws([&] { sceSslGetNotAfter(id, nullptr, &limit); }); + Throws([&] { sceSslGetNotAfter(id, fake, &limit); }); + Throws([&] { sceSslGetNotAfter(9999, cert, &limit); }); + + unsigned char md5[16]{}; + Check(sceSslGetFingerprint(id, cert, 0, md5) == 0, "get md5 fingerprint failed"); + constexpr unsigned char expectedMd5[16] = {0x71, 0x3c, 0xe5, 0x20, 0x88, 0x2b, 0x6b, 0xd0, 0x17, 0x85, 0x07, 0xde, 0xbd, 0xdc, 0xa9, 0x4f}; + Check(std::memcmp(md5, expectedMd5, sizeof(md5)) == 0, "md5 digest mismatch"); + + unsigned char sha1[20]{}; + Check(sceSslGetFingerprint(id, cert, 1, sha1) == 0, "get sha1 fingerprint failed"); + constexpr unsigned char expectedSha1[20] = {0x47, 0xe0, 0x89, 0xab, 0xb7, 0x02, 0xee, 0xb6, 0x2f, 0x8b, 0x5b, 0x7a, 0x13, 0xfe, 0x82, 0x6d, 0x2b, 0xe8, 0xee, 0xa7}; + Check(std::memcmp(sha1, expectedSha1, sizeof(sha1)) == 0, "sha1 digest mismatch"); + + unsigned char sha256[32]{}; + Check(sceSslGetFingerprint(id, cert, 3, sha256) == 0, "get sha256 fingerprint failed"); + constexpr unsigned char expectedSha256[32] = {0x3d, 0x7d, 0xac, 0xd3, 0x04, 0x90, 0x42, 0xe1, 0x57, 0x19, 0x87, 0x27, 0x54, 0x45, 0xae, 0xed, 0xc4, 0xea, 0xd6, 0xd4, 0x20, 0xa0, 0x7f, 0xc4, 0xf5, 0x26, 0xc1, 0xa0, 0x85, 0x87, 0x5a, 0x4c}; + Check(std::memcmp(sha256, expectedSha256, sizeof(sha256)) == 0, "sha256 digest mismatch"); + + Check(sceSslGetFingerprint(id, cert, 3, nullptr) == static_cast(0x8095177a), "null fingerprint buffer accepted"); + Check(sceSslGetFingerprint(id, cert, -1, sha256) == static_cast(0x8095177a), "negative hash type accepted"); + Check(sceSslGetFingerprint(id, cert, 99, sha256) == static_cast(0x8095177a), "invalid hash type accepted"); + Throws([&] { sceSslGetFingerprint(id, nullptr, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(9999, cert, 3, sha256); }); + + Throws([&] { sceSslClose(1); }); + + 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); }); + Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); + Throws([&] { sceSslGetNotAfter(id, cert, &limit); }); + Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); }); + + 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); }); + Throws([&] { sceSslGetNotBefore(id2, cert2, &begin); }); + Throws([&] { sceSslGetNotAfter(id2, cert2, &limit); }); + Throws([&] { sceSslGetFingerprint(id2, cert2, 3, sha256); }); + Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); + Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); + Throws([&] { sceSslFreeSslCertName(id2, subject2); }); +} + } int main() { @@ -165,6 +317,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/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp new file mode 100644 index 0000000000..ddbbd279aa --- /dev/null +++ b/core/libs/tests/GuestSslTransport.cpp @@ -0,0 +1,268 @@ +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI sceSslGetMemoryPoolStats(int, SceSsl::MemoryPoolStats*); +int APS5_VABI sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +int APS5_VABI sceSslConnect(int); +int APS5_VABI sceSslRead(int, void*, std::size_t, int); +int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +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); +} + +namespace { + +constexpr int ParallelConnections = 4; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +int APS5_VABI VerifyCallback(int, unsigned int, void**, int, void*) { return 0; } + +struct Server { + mbedtls_net_context listener; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + mbedtls_x509_crt certificate; + mbedtls_x509_crt authority; + mbedtls_pk_context key; + mbedtls_ssl_config config; + std::uint16_t port = 0; + std::string failure; + + Server() { + mbedtls_net_init(&listener); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&drbg); + mbedtls_x509_crt_init(&certificate); + mbedtls_x509_crt_init(&authority); + mbedtls_pk_init(&key); + mbedtls_ssl_config_init(&config); + Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); + Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, + "parse server certificate"); + Require(mbedtls_x509_crt_parse(&authority, reinterpret_cast(RootPem), sizeof(RootPem)) == 0, + "parse client authority"); + Require(mbedtls_pk_parse_key(&key, reinterpret_cast(ServerKey), sizeof(ServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse server key"); + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); + Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); + sockaddr_in address{}; +#ifdef _WIN32 + int size = sizeof(address); +#else + socklen_t size = sizeof(address); +#endif + Require(getsockname(listener.fd, reinterpret_cast(&address), &size) == 0, "read listener port"); + port = ntohs(address.sin_port); + } + + ~Server() { + mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&key); + mbedtls_x509_crt_free(&authority); + mbedtls_x509_crt_free(&certificate); + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + mbedtls_net_free(&listener); + } + + void Serve(bool handshakeSucceeds) { + mbedtls_net_context client; + mbedtls_net_init(&client); + mbedtls_ssl_context ssl; + mbedtls_ssl_init(&ssl); + if (mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) != 0 || mbedtls_ssl_setup(&ssl, &config) != 0) { + failure = "server accept or setup failed"; + } else { + mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); + const int handshake = mbedtls_ssl_handshake(&ssl); + if ((handshake == 0) != handshakeSucceeds) failure = "server handshake result differs, error " + std::to_string(handshake); + else if (handshake == 0) Exchange(ssl); + } + mbedtls_ssl_free(&ssl); + mbedtls_net_free(&client); + } + + void Exchange(mbedtls_ssl_context& ssl) { + std::array request{}; + std::size_t received = 0; + while (received < request.size()) { + const int result = mbedtls_ssl_read(&ssl, request.data() + received, request.size() - received); + if (result <= 0) { + failure = "server read failed, error " + std::to_string(result); + return; + } + received += static_cast(result); + } + if (std::memcmp(request.data(), "ping", 4) != 0) { + failure = "server received the wrong request"; + return; + } + if (mbedtls_ssl_write(&ssl, reinterpret_cast("pong"), 4) != 4) { + failure = "server write failed"; + return; + } + if (mbedtls_ssl_close_notify(&ssl) != 0) failure = "server close notify failed"; + } +}; + +struct Sockets { + bool net; + + int Open(std::uint16_t port) const { + const int descriptor = net ? sceNetSocket("ssl", 2, 1, 6) : socket_nid_postfix(2, 1, 6); + Require(descriptor >= 0, "create guest socket"); + std::array address{16, 2, static_cast(port >> 8), static_cast(port), 127, 0, 0, 1}; + Require((net ? sceNetConnect(descriptor, address.data(), 16) : connect_nid_postfix(descriptor, address.data(), 16)) == 0, + "connect guest socket"); + return descriptor; + } + + int Close(int descriptor) const { return net ? sceNetSocketClose(descriptor) : close_nid_postfix(descriptor); } +}; + +std::size_t InUse(int context) { + SceSsl::MemoryPoolStats stats{}; + Require(sceSslGetMemoryPoolStats(context, &stats) == 0, "read pool statistics"); + return stats.currentInuseSize; +} + +} + +int main(int argc, char** argv) { + Require(argc == 2 && (std::strcmp(argv[1], "kernel") == 0 || std::strcmp(argv[1], "net") == 0), "usage: kernel|net"); +#ifndef _WIN32 + std::signal(SIGPIPE, SIG_IGN); +#endif + const Sockets sockets{std::strcmp(argv[1], "net") == 0}; + if (sockets.net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Server server; + std::thread serving([&] { + server.Serve(false); + server.Serve(true); + for (int index = 0; index < ParallelConnections; ++index) server.Serve(true); + }); + + const int empty = sceSslInit_nid_postfix(256 * 1024); + const int withoutAuthority = sceSslCreateConnection(empty, -1, "localhost"); + Require(Throws([&] { sceSslConnect(withoutAuthority); }), "a handshake without a loaded CA was attempted"); + Require(sceSslDeleteConnection(withoutAuthority) == 0 && sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); + + const int context = sceSslInit_nid_postfix(1024 * 1024); + std::string root = RootPem; + SceSsl::Data authority{reinterpret_cast(root.data()), root.size()}; + auto* authorities = &authority; + std::string clientCertificate = ClientPem, clientKey = ClientKey; + SceSsl::Data certificate{reinterpret_cast(clientCertificate.data()), clientCertificate.size()}; + SceSsl::Data key{reinterpret_cast(clientKey.data()), clientKey.size()}; + Require(sceSslLoadCert(context, 1, &authorities, &certificate, &key) == 0, "load the fixture root and client certificate"); + const auto loaded = InUse(context); + + const int callback = sceSslCreateConnection(context, -1, "localhost"); + Require(sceSslSetVerifyCallback(callback, VerifyCallback, nullptr) == 0, "set verify callback"); + Require(Throws([&] { sceSslConnect(callback); }), "a verify callback was accepted"); + Require(sceSslDeleteConnection(callback) == 0, "delete callback connection"); + + const int closed = sceSslCreateConnection(context, 1000, "localhost"); + Require(Throws([&] { sceSslConnect(closed); }), "a handshake over a closed descriptor was attempted"); + Require(sceSslDeleteConnection(closed) == 0, "delete connection over a closed descriptor"); + + const int mismatchSocket = sockets.Open(server.port); + const int mismatch = sceSslCreateConnection(context, mismatchSocket, "example.com"); + Require(Throws([&] { sceSslConnect(mismatch); }), "a certificate for another host was accepted"); + Require(Throws([&] { sceSslRead(mismatch, root.data(), 1, 0); }), "a failed handshake left the connection usable"); + Require(sceSslDeleteConnection(mismatch) == 0 && sockets.Close(mismatchSocket) == 0, "release the mismatched connection"); + + const int descriptor = sockets.Open(server.port); + const int connection = sceSslCreateConnection(context, descriptor, "localhost"); + std::array buffer{}; + Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "read before the handshake"); + Require(Throws([&] { sceSslWrite(connection, buffer.data(), buffer.size(), 0); }), "write before the handshake"); + Require(sceSslConnect(connection) == 0, "handshake with the loopback server"); + Require(InUse(context) > loaded, "the connection is not charged to the context pool"); + Require(Throws([&] { sceSslConnect(connection); }), "a second handshake was accepted"); + Require(Throws([&] { sceSslWrite(connection, buffer.data(), 1, 1); }), "unknown write flags were accepted"); + Require(Throws([&] { sceSslRead(connection, buffer.data(), 1, 3); }), "peek was accepted"); + char request[] = "ping"; + Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); + Require(sceSslDeleteConnection(connection) == 0, "delete the connection"); + Require(InUse(context) == loaded, "the connection's pool memory was not returned"); + Require(sockets.Close(descriptor) == 0, "the socket did not stay open after the connection was deleted"); + + std::array clients; + for (auto& client : clients) { + client = std::thread([&] { + const int parallelSocket = sockets.Open(server.port); + const int parallel = sceSslCreateConnection(context, parallelSocket, "localhost"); + std::array reply{}; + char ping[] = "ping"; + Require(sceSslConnect(parallel) == 0 && sceSslWrite(parallel, ping, 4, 0) == 4, "parallel handshake or write"); + Require(sceSslRead(parallel, reply.data(), reply.size(), 0) == 4 && std::memcmp(reply.data(), "pong", 4) == 0, "parallel reply"); + Require(sceSslRead(parallel, reply.data(), reply.size(), 0) == 0, "parallel end of stream"); + Require(sceSslDeleteConnection(parallel) == 0 && sockets.Close(parallelSocket) == 0, "release parallel connection"); + }); + } + for (auto& client : clients) client.join(); + Require(InUse(context) == loaded, "parallel connections did not return their pool memory"); + + serving.join(); + Require(server.failure.empty(), server.failure.c_str()); + Require(sceSslTerm_nid_postfix(context) == 0, "terminate the context"); +} diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 9e99c39e09..dca63ca158 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -22,6 +22,9 @@ #endif extern "C" { FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +void APS5_VABI clearerr_unlocked_nid_postfix(FileStream*); +int APS5_VABI fileno_unlocked_nid_postfix(FileStream*); +char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*); 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*); FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); @@ -169,6 +172,22 @@ static void CheckUnlockedStreams() { Require(std::filesystem::remove(filename)); } +static void CheckUnlockedQueries() { + FileStream stream(std::tmpfile()); + Require(fputc_nid_postfix(0x7f, &stream) == 0x7f); + std::rewind(stream.GetHandle()); + Require(fgetc_nid_postfix(&stream) == 0x7f && feof_nid_postfix(&stream) == 0); + Require(fgetc_nid_postfix(&stream) == -1 && feof_nid_postfix(&stream) != 0); + clearerr_unlocked_nid_postfix(&stream); + Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0); + Require(fileno_unlocked_nid_postfix(&stream) == fileno_nid_postfix(&stream) && fileno_unlocked_nid_postfix(&stream) >= 0); + Require(fileno_unlocked_nid_postfix(nullptr) == -1); + bool halted = false; + std::size_t length = 0; + try { fgetln_nid_postfix(&stream, &length); } catch (const std::runtime_error&) { halted = true; } + Require(halted); +} + static void CheckTryLock() { FileStream stream(std::tmpfile()); const auto tryFromOtherThread = [&] { @@ -843,5 +862,6 @@ int main(int argc, char** argv) { CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); CheckTryLock(); + CheckUnlockedQueries(); return CheckBinaryModes() ? 0 : 1; } 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/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 9c59471e91..c94f557187 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -110,7 +110,8 @@ int main() { Require(pthread_getthreadid_np_nid_postfix() == static_cast(mainTid)); int local = 0; Require(StackContains(mainSelf, &local)); - Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(mainSelf, nullptr) == GUEST_EDEADLK); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadJoin(nullptr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadDetach(nullptr) == SCE_KERNEL_ERROR_EINVAL); 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/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 83d7afc731..4e6e1809a4 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -2,6 +2,7 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include extern "C" { int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); @@ -10,9 +11,11 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(); int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoom(); int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, std::int32_t* value); } @@ -83,4 +86,19 @@ int main() { Require(setting.get(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK); Require(value == setting.value); } + bool threwKeyremap = false; + try { + sceUserServiceGetAccessibilityKeyremapEnable(); + } catch (const std::runtime_error&) { + threwKeyremap = true; + } + Require(threwKeyremap); + + bool threwZoom = false; + try { + sceUserServiceGetAccessibilityZoom(); + } catch (const std::runtime_error&) { + threwZoom = true; + } + Require(threwZoom); } 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/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 39b16ff31e..6c404d7bce 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -8,8 +8,16 @@ #include #include #include +#include extern "C" { +std::size_t APS5_VABI c32rtomb_nid_postfix(char* destination, std::uint32_t value, void* state); +std::size_t APS5_VABI c16rtomb_nid_postfix(char* destination, std::uint16_t unit, void* state); +std::size_t APS5_VABI mbrtoc32_nid_postfix(std::uint32_t* destination, const char* source, std::size_t count, void* state); +std::size_t APS5_VABI mbrtoc16_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state); +int APS5_VABI mblen_nid_postfix(const char* source, std::size_t count); +int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value); +int* APS5_VABI __error_nid_postfix(); const char16_t* APS5_VABI wmemchr_nid_postfix(const char16_t* s, char16_t c, std::size_t n); int APS5_VABI wmemcmp_nid_postfix(const char16_t* s1, const char16_t* s2, std::size_t n); char16_t* APS5_VABI wmemcpy_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); @@ -26,6 +34,7 @@ const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsstr_nid_postfix(const char16_t* haystack, const char16_t* needle); const char16_t* APS5_VABI wcspbrk_nid_postfix(const char16_t* s, const char16_t* accept); std::size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept); +std::size_t APS5_VABI wcscspn_nid_postfix(const char16_t* s, const char16_t* reject); char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last); char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, std::size_t n); double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr); @@ -33,6 +42,11 @@ float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr); long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr); long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base); long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base); +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base); +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base); +unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base); +char* APS5_VABI rindex_nid_postfix(const char* s, int c); +int APS5_VABI flsl_nid_postfix(std::int64_t mask); unsigned long long APS5_VABI wcstoul_nid_postfix(const char16_t* str, char16_t** endptr, int base); unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t** endptr, int base); int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second); @@ -55,6 +69,54 @@ bool same(const char16_t* left, const char16_t* right, std::size_t count) { } int main() { + { + const auto bytesOf = [](const char* text, std::size_t count) { return std::string(text, count); }; + char out[8] = {}; + require(c32rtomb_nid_postfix(out, 0x41, nullptr) == 1 && out[0] == 'A'); + require(c32rtomb_nid_postfix(out, 0xe9, nullptr) == 2 && bytesOf(out, 2) == "\xc3\xa9"); + require(c32rtomb_nid_postfix(out, 0x20ac, nullptr) == 3 && bytesOf(out, 3) == "\xe2\x82\xac"); + require(c32rtomb_nid_postfix(out, 0x1f600, nullptr) == 4 && bytesOf(out, 4) == "\xf0\x9f\x98\x80"); + require(c32rtomb_nid_postfix(out, 0x7fffffff, nullptr) == 6 && bytesOf(out, 6) == "\xfd\xbf\xbf\xbf\xbf\xbf"); + require(c32rtomb_nid_postfix(nullptr, 0x20ac, nullptr) == 1); + + alignas(8) unsigned char state[16] = {}; + require(c16rtomb_nid_postfix(out, 0xd83d, state) == 0 && state[10] == 1); + require(c16rtomb_nid_postfix(out, 0x41, state) == static_cast(-1) && state[10] == 1); + require(c16rtomb_nid_postfix(nullptr, 0x41, state) == static_cast(-1)); + require(c16rtomb_nid_postfix(out, 0xde00, state) == 4 && bytesOf(out, 4) == "\xf0\x9f\x98\x80" && state[10] == 0); + require(c16rtomb_nid_postfix(out, 0xdc00, state) == 3 && bytesOf(out, 3) == "\xed\xb0\x80"); + require(c16rtomb_nid_postfix(out, 0x7f, state) == 1 && out[0] == 0x7f); + + std::memset(state, 0, sizeof(state)); + std::uint32_t wide = 0; + require(mbrtoc32_nid_postfix(&wide, "\xf0\x9f\x98\x80z", 5, state) == 4 && wide == 0x1f600); + require(mbrtoc32_nid_postfix(&wide, "\xe2\x82", 2, state) == static_cast(-2) && state[10] == 1); + require(mbrtoc32_nid_postfix(&wide, "\xac", 1, state) == 1 && wide == 0x20ac && state[10] == 0); + require(mbrtoc32_nid_postfix(&wide, "", 1, state) == 0 && wide == 0); + require(mbrtoc32_nid_postfix(&wide, "A", 0, state) == static_cast(-2)); + wide = 7; + require(mbrtoc32_nid_postfix(&wide, nullptr, 9, state) == 0 && wide == 7); + *__error_nid_postfix() = 0; + require(mbrtoc32_nid_postfix(&wide, "\x80", 1, state) == static_cast(-1) && *__error_nid_postfix() == 86); + require(mbrtoc32_nid_postfix(&wide, "\xfd\xbf\xbf\xbf\xbf\xbf", 6, state) == 6 && wide == 0x7fffffff); + + std::memset(state, 0, sizeof(state)); + std::uint16_t unit = 0; + require(mbrtoc16_nid_postfix(&unit, "\xc3\xa9", 2, state) == 2 && unit == 0xe9); + require(mbrtoc16_nid_postfix(&unit, "\xf0\x9f", 2, state) == static_cast(-2)); + require(mbrtoc16_nid_postfix(&unit, "\x98\x80", 2, state) == 2 && unit == 0xd83d && state[10] == 0x10); + require(mbrtoc16_nid_postfix(&unit, "A", 1, state) == static_cast(-3) && unit == 0xdc1f && state[10] == 0); + std::memset(state, 0, sizeof(state)); + require(mbrtoc16_nid_postfix(&unit, "\xf0\x9f\x98\x80", 4, state) == 4 && unit == 0xd83d); + require(mbrtoc16_nid_postfix(&unit, nullptr, 0, state) == static_cast(-3) && unit == 0xd83d); + *__error_nid_postfix() = 0; + require(mbrtoc16_nid_postfix(&unit, "\xf4\x90\x80\x80", 4, state) == static_cast(-1) && *__error_nid_postfix() == 86); + + require(mblen_nid_postfix(nullptr, 1) == 0 && mblen_nid_postfix("ab", 2) == 1 && mblen_nid_postfix("", 1) == 0); + require(wctomb_nid_postfix(nullptr, 0x41) == 0); + require(wctomb_nid_postfix(out, 0x42) == 1 && out[0] == 'B'); + require(wctomb_nid_postfix(out, 0x263a) == -1); + } const char16_t split[] = {u'a', u'b', 0, u'c', 0}; require(wcslen_nid_postfix(u"") == 0); require(wcslen_nid_postfix(u"héllo") == 5); @@ -118,6 +180,7 @@ int main() { require(wcspbrk_nid_postfix(haystack, u"xyz") == nullptr); require(wcsspn_nid_postfix(u"aabbc", u"ab") == 4); require(wcsspn_nid_postfix(u"abc", u"") == 0); + require(wcscspn_nid_postfix(u"abcde", u"dx") == 3 && wcscspn_nid_postfix(u"abc", u"") == 3 && wcscspn_nid_postfix(u"", u"a") == 0 && wcscspn_nid_postfix(u"abc", u"a") == 0); char16_t tokens[] = u",,one,,two;three,"; char16_t* last = tokens; @@ -238,5 +301,21 @@ int main() { require(same(transformed, test.expected, test.written)); for (std::size_t index = test.written; index < 8; ++index) require(transformed[index] == 0xaaaa); } + { + const char16_t number[] = u" -0x7fffffffffffffff9"; + char16_t* wideEnd = nullptr; + require(wcstoimax_nid_postfix(number, &wideEnd, 0) == INT64_MIN && wideEnd == number + 22); + const char16_t positive[] = u"+42z"; + require(wcstoumax_nid_postfix(positive, &wideEnd, 10) == 42 && wideEnd == positive + 3); + require(wcstoumax_nid_postfix(u"-1", nullptr, 10) == UINT64_MAX); + char* narrowEnd = nullptr; + const char text[] = "0x1ffffffffffffffffg"; + require(strtouq_nid_postfix(text, &narrowEnd, 16) == UINT64_MAX && narrowEnd == text + 19); + const char path[] = "a/b/c"; + require(rindex_nid_postfix(path, '/') == path + 3 && rindex_nid_postfix(path, 'x') == nullptr); + require(rindex_nid_postfix(path, 0) == path + 5); + require(flsl_nid_postfix(0) == 0 && flsl_nid_postfix(1) == 1 && flsl_nid_postfix(0x80) == 8); + require(flsl_nid_postfix(0x100000000ll) == 33 && flsl_nid_postfix(INT64_MIN) == 64 && flsl_nid_postfix(-1) == 64); + } return 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/Mspace.cpp b/core/libs/tests/Mspace.cpp index 386435ded6..2224e2b7a1 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -22,6 +23,15 @@ void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void*, std::size_t, std::s void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); +void* APS5_VABI sceLibcPafMspaceCreate_nid_postfix(const char*, void*, std::size_t, unsigned, const void*); +int APS5_VABI sceLibcPafMspaceDestroy_nid_postfix(void*); +void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void*, std::size_t, const void*); +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void*, void*, const void*); +void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void*, void*, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } struct MallocManagedSize { std::uint16_t size; @@ -35,6 +45,38 @@ struct MallocManagedSize { static void Require(bool value) { if (!value) { std::fputs("Mspace check failed\n", stderr); std::abort(); } } +static void CheckPaf() { + alignas(64) static std::array storage{}; + bool rejected = false; + int options = 0; + try { sceLibcPafMspaceCreate_nid_postfix("paf", storage.data(), storage.size(), 0, &options); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); + void* arena = sceLibcPafMspaceCreate_nid_postfix("paf", storage.data(), storage.size(), 0, nullptr); + Require(arena == storage.data()); + const int owner = 0; + auto* block = static_cast(sceLibcPafMspaceMalloc_nid_postfix(arena, 40, &owner)); + Require(block != nullptr && sceLibcMspaceMallocUsableSize_nid_postfix(block) >= 40); + std::memset(block, 0x5a, 40); + auto* zeroed = static_cast(sceLibcPafMspaceCalloc_nid_postfix(arena, 4, 8, arena)); + Require(zeroed != nullptr && zeroed[0] == 0 && zeroed[31] == 0); + block = static_cast(sceLibcPafMspaceRealloc_nid_postfix(arena, block, 4000, &owner)); + Require(block != nullptr && block[0] == 0x5a && block[39] == 0x5a); + block = static_cast(sceLibcPafMspaceReallocalign_nid_postfix(arena, block, 256, 64, &owner)); + Require(block != nullptr && reinterpret_cast(block) % 256 == 0 && block[39] == 0x5a); + MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; + viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); + viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; + Require(sceLibcPafMspaceMallocStats_nid_postfix(arena, &viaPaf) == sceLibcMspaceMallocStats_nid_postfix(arena, &viaMspace)); + Require(sceLibcPafMspaceMallocStatsFast_nid_postfix(arena, &fastPaf) == sceLibcMspaceMallocStatsFast_nid_postfix(arena, &fastMspace)); + Require(viaPaf.currentInuseSize == viaMspace.currentInuseSize && viaPaf.currentInuseSize > 0); + Require(fastPaf.currentInuseSize == fastMspace.currentInuseSize); + sceLibcPafMspaceFree_nid_postfix(arena, block, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, zeroed, arena); + sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); +} static void CheckAlignment() { alignas(32) static std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("aligned", storage.data() + 16, 65536, 0); @@ -52,7 +94,7 @@ static void CheckAlignment() { Require(aligned(blocks[0])); void* small = sceLibcMspaceMemalign_nid_postfix(arena, 16, 40); Require(aligned(small)); - void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 40, 8); + void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 8, 40); Require(aligned(moved)); void* posix = nullptr; Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &posix, 8, 40) == 0 && aligned(posix)); @@ -102,11 +144,12 @@ static void CheckMovedBlockKeepsUsableBytes() { void* other = sceLibcMspaceMalloc_nid_postfix(arena, 1); const auto otherUsable = fill(other); Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr); - void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 4096, 256); + void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 256, 4096); Require(realigned != nullptr && realigned != other && kept(realigned, otherUsable)); Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); } int main() { + CheckPaf(); CheckAlignment(); CheckAdjacentBlocks(); CheckMovedBlockKeepsUsableBytes(); @@ -126,7 +169,7 @@ int main() { Require(sceLibcMspaceRealloc_nid_postfix(arena, first, size) == nullptr); Require(*__error_nid_postfix() == 12); *__error_nid_postfix() = 0; - Require(sceLibcMspaceReallocalign_nid_postfix(arena, first, size, 16) == nullptr); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, first, 16, size) == nullptr); Require(*__error_nid_postfix() == 12); Require(sceLibcMspaceMallocUsableSize_nid_postfix(first) == 128); for (int i = 0; i < 128; ++i) Require(first[i] == i); @@ -194,12 +237,17 @@ int main() { unsigned char* realigned = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64)); Require(realigned && (reinterpret_cast(realigned) & 63) == 0); for (int i = 0; i < 64; ++i) realigned[i] = static_cast(i + 1); - unsigned char* regrown = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 256, 64)); + unsigned char* regrown = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 64, 256)); Require(regrown && (reinterpret_cast(regrown) & 63) == 0); for (int i = 0; i < 64; ++i) Require(regrown[i] == static_cast(i + 1)); - Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 0) == nullptr); - Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 3) == nullptr); - Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == nullptr); + *__error_nid_postfix() = 0; + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == regrown); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 3, 16) == regrown); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, std::size_t{1} << 63, 16) == nullptr); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 64, 0) == regrown && regrown[63] == 64); + Require(*__error_nid_postfix() == 0); + unsigned char* rounded = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 100, 0)); + Require(rounded && (reinterpret_cast(rounded) & 127) == 0); Require(sceLibcMspaceMallocStatsFast_nid_postfix(arena, &stats) == 0 && stats.currentInuseSize >= 100); sceLibcMspaceFree_nid_postfix(arena, memaligned); void* region = sceLibcMspaceMalloc_nid_postfix(arena, 8192); 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/ProcessParametersFixture.hpp b/core/libs/tests/ProcessParametersFixture.hpp new file mode 100644 index 0000000000..794ba40ac9 --- /dev/null +++ b/core/libs/tests/ProcessParametersFixture.hpp @@ -0,0 +1,91 @@ +#ifndef CORE_LIBS_TESTS_PROCESSPARAMETERSFIXTURE_HPP +#define CORE_LIBS_TESTS_PROCESSPARAMETERSFIXTURE_HPP + +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__linux__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include +#include +#endif + +namespace ProcessParametersFixture { +namespace { + +#ifdef _WIN32 +[[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t metadata[2]{}; +#endif + +bool Install(std::byte* process, std::size_t size) { +#ifdef _WIN32 + auto* image = reinterpret_cast(GetModuleHandleW(nullptr)); + if (image == nullptr) return false; + auto* dos = reinterpret_cast(image); + auto* nt = reinterpret_cast(image + dos->e_lfanew); + auto* sections = IMAGE_FIRST_SECTION(nt); + IMAGE_SECTION_HEADER* target = nullptr; + for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) { + if (std::memcmp(sections[index].Name, ".proctst", 8) == 0) { + target = §ions[index]; + break; + } + } + if (target == nullptr) return false; + metadata[0] = static_cast(process - image); + metadata[1] = static_cast(size); + DWORD oldProtect = 0; + if (VirtualProtect(target, sizeof(*target), PAGE_READWRITE, &oldProtect) == 0) return false; + std::memcpy(target->Name, ".procpar", 8); + target->Misc.VirtualSize = sizeof(metadata); + DWORD restored = 0; + VirtualProtect(target, sizeof(*target), oldProtect, &restored); + return true; +#elif defined(__linux__) + struct Request { + std::byte* process; + std::size_t size; + bool installed; + } request{process, size, false}; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { + auto& request = *static_cast(data); + if (image->dlpi_name != nullptr && image->dlpi_name[0] != '\0') return 0; + for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type == PT_LOAD || header.p_type == PT_GNU_EH_FRAME || header.p_type < 0x60000000u) continue; + auto* mutableHeader = const_cast(&header); + const auto pageSize = static_cast(sysconf(_SC_PAGESIZE)); + const auto pageStart = reinterpret_cast(mutableHeader) & ~(pageSize - 1); + const auto pageEnd = (reinterpret_cast(mutableHeader + 1) + pageSize - 1) & ~(pageSize - 1); + int protection = PROT_READ; + for (std::uint16_t loadIndex = 0; loadIndex < image->dlpi_phnum; ++loadIndex) { + const auto& load = image->dlpi_phdr[loadIndex]; + const auto start = image->dlpi_addr + load.p_vaddr; + if (load.p_type == PT_LOAD && pageStart >= (start & ~(pageSize - 1)) && pageStart < start + load.p_memsz && (load.p_flags & PF_X)) protection |= PROT_EXEC; + } + if (mprotect(reinterpret_cast(pageStart), pageEnd - pageStart, protection | PROT_WRITE) != 0) return 1; + mutableHeader->p_type = 0x61000001; + mutableHeader->p_vaddr = reinterpret_cast(request.process) - image->dlpi_addr; + mutableHeader->p_paddr = mutableHeader->p_vaddr; + mutableHeader->p_filesz = request.size; + mutableHeader->p_memsz = request.size; + request.installed = mprotect(reinterpret_cast(pageStart), pageEnd - pageStart, protection) == 0; + return 1; + } + return 1; + }, &request); + return request.installed; +#else + return false; +#endif +} + +} +} + +#endif 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/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp index 82d0ad962b..bc5637db37 100644 --- a/core/libs/tests/SslCertificateFixture.hpp +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -71,3 +71,23 @@ inline constexpr std::array RootDer{ 0x78, 0xd6, 0xd3, 0xf7, 0x39, 0x5b, 0x74, 0x8f, 0xae, 0xa3, 0x9d, 0x2e, 0xb4, 0xcf, 0x6b, 0x0d, 0x9f, 0x24, 0x48, 0x1c, 0x79, 0x46, 0xcb, 0x80, 0x56, 0x3f, 0x0c, 0xdf, }; +inline constexpr char ServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIB+TCCAZ6gAwIBAgIDXhfhMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDE2MTEyOTUzWhcNMzYxMDEzMTEyOTUzWjA4MQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAASOWLxgY2XcCm5WcZKzafvQHUuJX1KtbMsN +IbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyBo4GMMIGJMBQG +A1UdEQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH +gDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUuuGJo54FNP2gUW5R7mLC +BAz9xkMwHwYDVR0jBBgwFoAUVmiVtNwaphUy1PoEPfuD3KpoDNwwCgYIKoZIzj0E +AwIDSQAwRgIhAM3tCykI/0NBGJMs+4wsjT835nfuzudPExaxh7XrfjyjAiEAn++S +D83KL8LSeGSsh10kVT7dM8acB2f3gQ9c0m+h3+Q= +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggjpMtOf9m8ZADfb/ +g+KCUQ46CTPPM2bFyypaJZ3ZJq2hRANCAASOWLxgY2XcCm5WcZKzafvQHUuJX1Kt +bMsNIbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyB +-----END PRIVATE KEY----- +)FIXTURE"; 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/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index 99a3ca9282..19a4bb2e86 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -57,7 +57,7 @@ static void TestTypeInfoVtables() { if (catchType(&other, &single, &object, 0)) throw std::runtime_error("unrelated RTTI catch succeeded"); } -extern "C" void NotImplemented_nid_no_patch(const char*); +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char*); static void RequireHandle(bool value) { if (!value) throw std::runtime_error("child process setup failed"); diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 2093126aa4..0588e469d0 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -231,6 +231,21 @@ void CheckProtection() { Require(!GuestWriteWatchCovers_nid_postfix(region.Address(2), region.page), "an unregistered range is covered"); } +void CheckOverlaps() { + Region region(6); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(0), region.bytes), "an unregistered range overlaps the watch"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(GuestWriteWatchUnregister_nid_postfix(region.base, region.page), "cannot unregister the first page"); + Require(GuestWriteWatchUnregister_nid_postfix(region.base + 3 * region.page, 3 * region.page), "cannot unregister the last pages"); + Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "a range around unregistered pages is covered"); + Require(GuestWriteWatchOverlaps_nid_postfix(region.Address(0), region.bytes), "a range around registered pages does not overlap the watch"); + Require(GuestWriteWatchOverlaps_nid_postfix(region.Address(1) - 1, 2), "a range ending in a registered page does not overlap the watch"); + Require(GuestWriteWatchOverlaps_nid_postfix(region.Address(3) - 1, 2), "a range starting in a registered page does not overlap the watch"); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(0), region.page), "an unregistered page before the registered ones overlaps the watch"); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(3), 3 * region.page), "unregistered pages after the registered ones overlap the watch"); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(1), 0), "an empty range overlaps the watch"); +} + void CheckThreads() { constexpr std::size_t Threads = 4; constexpr std::size_t PagesPerThread = 64; @@ -269,7 +284,7 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection, overlaps or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); else if (mode == "fresh") CheckFresh(); @@ -279,6 +294,7 @@ int main(int argc, char** argv) { else if (mode == "host") CheckHostWrite(); else if (mode == "empty") CheckEmptyRanges(); else if (mode == "protection") CheckProtection(); + else if (mode == "overlaps") CheckOverlaps(); else if (mode == "threads") CheckThreads(); else throw std::runtime_error("unknown write watch test"); std::printf("write watch %s passed\n", argv[1]); diff --git a/core/libs/tests/shared_memory/Support.cpp b/core/libs/tests/shared_memory/Support.cpp index 03f202e4ef..e9df457817 100644 --- a/core/libs/tests/shared_memory/Support.cpp +++ b/core/libs/tests/shared_memory/Support.cpp @@ -6,6 +6,6 @@ extern "C" int* APS5_VABI __error_nid_postfix() { return &error; } -extern "C" void NotImplemented_nid_no_patch(const char* name) { +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* name) { throw std::runtime_error(name); } diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index 7e55f2bb41..ba692f336c 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 empty protection threads) +foreach(mode collect fresh armed register mapped host empty protection overlaps 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..11c7ea959a 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) @@ -716,4 +735,16 @@ if(BUILD_TESTING) ) add_test(NAME reloc_table_bounds COMMAND reloc_table_bounds_tests $) endif() + + set(relinkerTestTemp ${CMAKE_CURRENT_BINARY_DIR}/test-temp) + file(MAKE_DIRECTORY ${relinkerTestTemp}) + get_property(relinkerTests DIRECTORY PROPERTY TESTS) + set_property(TEST ${relinkerTests} APPEND PROPERTY ENVIRONMENT_MODIFICATION + "TMPDIR=set:${relinkerTestTemp};TEMP=set:${relinkerTestTemp};TMP=set:${relinkerTestTemp}") +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..75f6a899e1 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -4,9 +4,15 @@ #include #include #include +#include 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(); @@ -20,7 +26,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil const bool hasPlural = std::filesystem::exists(plural); const bool hasPrx = std::filesystem::exists(prx); if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist in the guest module parent directory"); - if (!hasSingular && !hasPlural && !hasPrx) throw Domain::RelinkerException(missingModuleMessage); + if (!hasSingular && !hasPlural && !hasPrx && moduleDirectories.empty()) throw Domain::RelinkerException(missingModuleMessage); std::vector directories; if (hasSingular || hasPlural) directories.push_back(hasSingular ? singular : plural); if (hasPrx) directories.push_back(prx); @@ -32,7 +38,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); } @@ -72,6 +78,31 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil result.Root = root; result.RuntimeDirectories = std::move(runtimeDirectories); for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); + std::map sonames; + const auto soname = [&](std::size_t index) -> const std::string& { + const auto [found, inserted] = sonames.try_emplace(index); + if (inserted) found->second = GuestImageReader().Read(result.Modules[index].Path, result.Modules[index].Bytes).Soname; + return found->second; + }; + std::vector copies(result.Modules.size()); + for (std::size_t copy = 0; copy < result.Modules.size(); ++copy) { + const auto& path = result.Modules[copy].Path; + const auto name = Io::Utf8Path(path.filename()); + for (std::size_t original = 0; original < result.Modules.size(); ++original) { + const auto& kept = result.Modules[original].Path; + const auto prefix = Io::Utf8Path(kept.filename()); + if (kept.parent_path() != path.parent_path() || prefix.size() >= name.size() || !name.starts_with(prefix)) continue; + if (soname(copy).empty() || soname(copy) != soname(original)) continue; + copies[copy] = true; + std::cout << "Skipping backup copy: " << path.lexically_relative(root).generic_string() << " (same SONAME as " + << kept.lexically_relative(root).generic_string() << ")\n"; + break; + } + } + std::vector modules; + for (std::size_t index = 0; index < result.Modules.size(); ++index) + if (!copies[index]) modules.push_back(std::move(result.Modules[index])); + result.Modules = std::move(modules); return result; } 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/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index 43e5a7110b..72bace4267 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -71,6 +71,7 @@ struct DecodedInstructionInfo { std::uint8_t ModRmByte; bool HasBranchTarget; std::int64_t BranchDisp; + bool TruncatesBranchTarget; bool IsTwoByteOpcode; std::uint8_t Opcode; std::uint8_t ModRmRegField; 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/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index f92c5edc93..a2ea235db6 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -55,12 +55,16 @@ public: ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} [[nodiscard]] std::optional Window(const std::size_t index) const { - if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) { - if (auto placement = _relocate(index, index, index + 1)) - return placement; + for (const bool avoidSegment : {true, false}) { + if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true, avoidSegment))) { + if (auto placement = _relocate(index, index, index + 1)) + return placement; + } + if (index > 0 && _movable(index - 1, false, avoidSegment)) { + if (auto placement = _relocate(index, index - 1, index)) + return placement; + } } - if (index > 0 && _movable(index - 1, false)) - return _relocate(index, index - 1, index); return std::nullopt; } @@ -108,8 +112,8 @@ private: return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; } - [[nodiscard]] bool _movable(const std::size_t index, const bool last) const { - return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)); + [[nodiscard]] bool _movable(const std::size_t index, const bool last, const bool avoidSegment = false) const { + return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)) && !(avoidSegment && _info(index).SegmentPrefix != 0); } [[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const { 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/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index e68805215f..f3e18ab213 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -210,6 +210,8 @@ void StubBodyBuilder::MoveControlFlow(const std::span instru const auto end = _siteOffset + static_cast(instruction.size()); if (info.FlowKind == ControlFlowKind::IndirectCall) throw CodegenException("A register or memory call cannot move into a stub"); + if (info.TruncatesBranchTarget) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); if (info.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { _pushReturnAddress(end); std::vector jump(instruction.begin(), instruction.end()); diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index a3f7f3962d..c6cc9de4ac 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -207,7 +207,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } if (opcode == OneByteCallRel32 || opcode == OneByteJmpRel32) { - immediateSize = ImmSize32; + immediateSize = operandImmediateSize; } if (opcode == OneByteJmpRel8 || opcode == OneByteJrcxz || @@ -241,7 +241,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { - immediateSize = ImmSize32; + immediateSize = operandImmediateSize; } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; @@ -355,6 +355,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Length = Decode(data, available); std::size_t pos = 0; + bool operandSizeOverride = false; while (pos < info.Length) { std::uint8_t b = data[pos]; @@ -371,6 +372,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.SegmentPrefix = b; if (b == PrefixAddressSize) info.AddressSize32 = true; + if (b == PrefixOperandSize) + operandSizeOverride = true; info.RexPrefix = 0; ++pos; continue; @@ -417,6 +420,19 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Opcode = op; info.IsTwoByteOpcode = twoByteOpcode; + const bool relativeNearBranch = twoByteOpcode + ? op >= TwoByteJccRel32Min && op <= TwoByteJccRel32Max + : op == OneByteCallRel32 || op == OneByteJmpRel32 || op == OneByteJmpRel8 || + (op >= OneByteJccRel8Min && op <= OneByteJccRel8Max) || + op == OneByteJrcxz || (op >= OneByteLoop && op <= OneByteLoopMax); + if (relativeNearBranch && operandSizeOverride && (info.RexPrefix & RexWBit) == 0) { + info.TruncatesBranchTarget = true; + info.FlowKind = twoByteOpcode || (op != OneByteCallRel32 && op != OneByteJmpRel32 && op != OneByteJmpRel8) + ? ControlFlowKind::ConditionalBranch + : op == OneByteCallRel32 ? ControlFlowKind::Call : ControlFlowKind::UnconditionalJump; + return info; + } + if (!twoByteOpcode) { if (op == OneByteCallRel32) { std::int32_t disp = 0; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index dcb82e697d..6fbecaf06b 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 @@ -825,6 +825,19 @@ void converterWideStoreWindows() { require(!preceded.Trampolines.empty() && preceded.Trampolines[0].Offset == 0x200 && preceded.Trampolines[0].Length == 8 && body(preceded.Trampolines[0]) == expected, "A 256-bit store moved a following instruction that cannot leave its place instead of the one before it"); } + const Bytes fsRead = {0x66, 0x66, 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00}; + const Bytes gsRead = {0x65, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00}; + for (const auto& segmentRead : {fsRead, gsRead}) { + const auto beforeSegment = convert(withStore(add, segmentRead)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(beforeSegment.Trampolines.size() == 1 && beforeSegment.Trampolines[0].Offset == 0x200 && beforeSegment.Trampolines[0].Length == 8 && body(beforeSegment.Trampolines[0]) == expected, "A 256-bit store moved a following FS or GS access, which the macOS writer cannot rewrite in a stub, instead of the instruction before it"); + const auto onlySegment = convert(withStore({}, segmentRead)); + expected = halves; + expected.insert(expected.end(), segmentRead.begin(), segmentRead.end()); + require(onlySegment.Trampolines.size() == 1 && onlySegment.Trampolines[0].Offset == 0x200 && onlySegment.Trampolines[0].Length == store.size() + segmentRead.size() && body(onlySegment.Trampolines[0]) == expected, "A 256-bit store with no other movable neighbour did not fall back to the following FS or GS access"); + } + Bytes throwing = {0xC3, 0xEB, 0xFD}; for (int index = 0; index < 3; ++index) throwing.insert(throwing.end(), add.begin(), add.end()); @@ -969,7 +982,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 +990,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 +1302,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 +1784,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/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 10747c9a61..1de81e75ab 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -420,6 +420,9 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"660f79c1", 4}, {"660f7c846511223344", 9}, {"660f7d846511223344", 9}, + {"660f801122", 5}, + {"660f841122", 5}, + {"660f8f1122", 5}, {"660fd0846511223344", 9}, {"660fd6846511223344", 9}, {"660fe6846511223344", 9}, @@ -430,6 +433,7 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"66358465", 4}, {"663d8465", 4}, {"66480584651122", 7}, + {"66480f8411223344", 8}, {"66480d84651122", 7}, {"66481584651122", 7}, {"66481d84651122", 7}, @@ -450,6 +454,8 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"6648be8465112233445566", 11}, {"6648bf8465112233445566", 11}, {"6648c784651122334455667708", 13}, + {"6648e811223344", 7}, + {"6648e911223344", 7}, {"6648f784651122334455667708", 13}, {"66688465", 4}, {"66698465112233445566", 10}, @@ -464,6 +470,8 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"66be8465", 4}, {"66bf8465", 4}, {"66c78465112233445566", 10}, + {"66e81122", 4}, + {"66e91122", 4}, {"66f78465112233445566", 10}, {"676984651122334455667708", 12}, {"678184651122334455667708", 12}, diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index 54dd90df2b..63bca4bfe0 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -99,6 +99,20 @@ int main() { require(info.HasBranchTarget == testCase.HasBranchTarget, "Wrong branch target presence for " + name); require(!testCase.HasBranchTarget || info.BranchDisp == testCase.BranchDisp, "Wrong branch displacement for " + name); } + for (const auto& testCase : kDecoderTruncatedBranchCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Truncated 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.TruncatesBranchTarget && !info.HasBranchTarget, "Branch target of " + name + " was reported without its 16-bit truncation"); + } + for (const auto& testCase : kDecoderBranchCases) { + const auto bytes = parse(testCase.Bytes); + require(!decoder.DecodeInstruction(bytes.data(), bytes.size()).TruncatesBranchTarget, "Branch target of " + std::string(testCase.Bytes) + " was reported as truncated"); + } 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 index c685b184ea..d29d0c0191 100644 --- a/core/relinker/codegen/tests/DecoderMetadataCases.hpp +++ b/core/relinker/codegen/tests/DecoderMetadataCases.hpp @@ -787,6 +787,33 @@ inline constexpr DecoderBranchCase kDecoderBranchCases[] = { {"ff28", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, {"ffd0", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, {"ffe0", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"66480f84c0111111", 8, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"6648e8c0111111", 7, Codegen::ControlFlowKind::Call, true, 286331328}, + {"6648e9c0111111", 7, Codegen::ControlFlowKind::UnconditionalJump, true, 286331328}, + {"6648ebc0", 4, Codegen::ControlFlowKind::UnconditionalJump, true, -64}, + {"664875c0", 4, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, +}; + +struct DecoderTruncatedBranchCase { + const char* Bytes; + std::size_t Length; + Codegen::ControlFlowKind FlowKind; +}; + +inline constexpr DecoderTruncatedBranchCase kDecoderTruncatedBranchCases[] = { + {"660f80c011", 5, Codegen::ControlFlowKind::ConditionalBranch}, + {"660f8fc011", 5, Codegen::ControlFlowKind::ConditionalBranch}, + {"6670c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"6675c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"667fc0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e0c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e2c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e3c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e8c011", 4, Codegen::ControlFlowKind::Call}, + {"66e9c011", 4, Codegen::ControlFlowKind::UnconditionalJump}, + {"66ebc0", 3, Codegen::ControlFlowKind::UnconditionalJump}, + {"6667e8c011", 5, Codegen::ControlFlowKind::Call}, + {"6640e8c011", 5, Codegen::ControlFlowKind::Call}, }; #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..19ff13e378 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; @@ -152,8 +158,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); const bool relativeBranch = - (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || - (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); + (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && !next.TruncatesBranchTarget && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || + (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.TruncatesBranchTarget && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)) throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length); std::vector movedBytes(bytes + length, bytes + length + next.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/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/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..ef20c1c1a1 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,8 @@ private: append(key, value.registrationWordLimit); append(key, value.vertexSubgroups); append(key, value.float16DenormPreserve); + append(key, value.spannedWaves); + append(key, value.robustBufferAccess2); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 20c4e29f51..bd80f9bbd6 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -239,7 +239,7 @@ ConservativeZExport readConservativeZExport(Reader& reader) { ColorExportPacking readColorExportPacking(Reader& reader) { const auto value = reader.ReadU8(); - if (value > static_cast(ColorExportPacking::Unorm10_11_11)) { + if (value > static_cast(ColorExportPacking::Float11_11_10)) { throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); } return static_cast(value); @@ -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/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index feb37aeb69..00894c6af1 100644 --- a/core/shader/recompiler/FragmentOutputSpecialization.hpp +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -32,7 +32,7 @@ inline std::vector SpecializeFragmentOutputs(std::vector= PipelineSpecialization::ExportPackingBase + 8u) continue; - if (constant.value > static_cast(ColorExportPacking::Unorm10_11_11)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value > static_cast(ColorExportPacking::Float11_11_10)) throw std::runtime_error("invalid prepared fragment export packing"); if (constant.value != static_cast(ColorExportPacking::None)) packed.insert(constant.id - PipelineSpecialization::ExportPackingBase); } if (packed.empty() && !dualSource && reversed == 0u) return words; @@ -81,7 +81,7 @@ inline std::vector SpecializeFragmentOutputs(std::vector +#include #include #include +#include #include #include #include @@ -151,6 +153,12 @@ struct CompactResourcePlan { std::vector conditions; }; +struct SrtTapeSlot { + std::once_flag once; + std::atomic ready{false}; + std::shared_ptr tape; +}; + struct IrResourcePlan { IrShaderStage stage = IrShaderStage::Unknown; std::uint64_t shaderHash = 0; @@ -177,6 +185,7 @@ struct IrResourcePlan { bool resourceTrackingComplete = false; ShaderInfo info; UniformFillPlan uniformFill; + std::shared_ptr tapeSlot = std::make_shared(); }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 85c985a879..4750cccb14 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -140,7 +140,7 @@ inline bool FoldsTexelOffsets(const SamplerResource& sampler) { } inline bool SamplesBaseLevelIfUnnormalized(const SamplerResource& sampler) { - return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; + return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod | SamplerUseOffset)) == 0u && !sampler.depthCompare; } struct SampledResourcePair { diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index d7f234f908..f5653e80f9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -65,10 +65,30 @@ void IrValue::ReplaceAllUsesWith(IrValue* replacement) { if (replacement == this) { return; } - while (!operandUses.empty()) { - const IrUse use = operandUses.back(); - use.user->ReplaceArgument(use.operand, replacement); + if (replacement != nullptr) { + const auto reserveAdditional = [count = operandUses.size()](auto& list) { + const auto maximum = list.max_size(); + if (count > maximum - list.size()) { + throw std::length_error("IrValue::ReplaceAllUsesWith use list is too large"); + } + const auto required = list.size() + count; + if (required > list.capacity()) { + const auto grown = list.capacity() > maximum / 2 ? maximum : list.capacity() * 2; + list.reserve(std::max(required, grown)); + } + }; + reserveAdditional(replacement->uses); + reserveAdditional(replacement->operandUses); } + for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) { + use->user->arguments[use->operand] = replacement; + if (replacement != nullptr) { + replacement->uses.push_back(use->user); + replacement->operandUses.push_back(*use); + } + } + uses.clear(); + operandUses.clear(); } void IrValue::SetImmediateU32(std::uint32_t value) { diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp new file mode 100644 index 0000000000..3308434316 --- /dev/null +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -0,0 +1,244 @@ +#include "IntermediateRepresentation/IrBlock.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace IrRewriteAllocation { +extern std::ptrdiff_t failureAfter; +extern std::size_t allocations; +extern bool countAllocations; +} +using namespace IrRewriteAllocation; + +void Require(bool condition) { + if (!condition) throw std::runtime_error("IR use rewrite assertion failed"); +} + +struct Graph { + IrBlock block{1}; + std::vector> values; + + explicit Graph(std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + auto value = std::make_unique(IrOpcode::IAdd32, IrType::U32, static_cast(index)); + value->SetFlags(static_cast(index * 17)); + block.AppendInstruction(value.get()); + values.push_back(std::move(value)); + } + } + + IrValue* Value(std::size_t index) const { return values.at(index).get(); } +}; + +void ReferenceReplace(IrValue* source, IrValue* replacement) { + if (source == replacement) return; + while (!source->OperandUses().empty()) { + const IrUse use = source->OperandUses().back(); + use.user->ReplaceArgument(use.operand, replacement); + } +} + +void EqualGraphs(const Graph& first, const Graph& second) { + Require(first.values.size() == second.values.size()); + for (std::size_t index = 0; index < first.values.size(); ++index) { + const auto* left = first.Value(index); + const auto* right = second.Value(index); + Require(left->Opcode() == right->Opcode() && left->Type() == right->Type()); + Require(left->Id() == right->Id() && left->Flags() == right->Flags()); + Require((left->Parent() == nullptr) == (right->Parent() == nullptr)); + if (left->Parent()) Require(left->Parent()->Id() == right->Parent()->Id()); + Require(left->ArgumentCount() == right->ArgumentCount()); + for (std::size_t operand = 0; operand < left->ArgumentCount(); ++operand) { + const auto* a = left->Argument(operand); + const auto* b = right->Argument(operand); + Require((a == nullptr) == (b == nullptr)); + if (a) Require(a->Id() == b->Id()); + } + Require(left->Uses().size() == right->Uses().size()); + for (std::size_t use = 0; use < left->Uses().size(); ++use) { + Require(left->Uses()[use]->Id() == right->Uses()[use]->Id()); + } + Require(left->OperandUses().size() == right->OperandUses().size()); + for (std::size_t use = 0; use < left->OperandUses().size(); ++use) { + const auto& a = left->OperandUses()[use]; + const auto& b = right->OperandUses()[use]; + Require(a.user->Id() == b.user->Id() && a.operand == b.operand); + } + } +} + +void CheckSimpleReplacement() { + Graph graph(5); + auto* source = graph.Value(0); + auto* target = graph.Value(1); + auto* first = graph.Value(2); + auto* second = graph.Value(3); + auto* existing = graph.Value(4); + existing->AddArgument(target); + first->AddArgument(source); + second->AddArgument(source); + first->AddArgument(source); + first->AddArgument(target); + source->ReplaceAllUsesWith(target); + Require(!source->HasUses() && source->OperandUses().empty()); + Require(first->Argument(0) == target && first->Argument(1) == target && first->Argument(2) == target); + Require(second->Argument(0) == target); + const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; + Require(target->OperandUses() == expected); + Require(target->Uses() == std::vector({existing, first, first, second, first})); + first->ReplaceArgument(1, source); + Require(source->UseCount() == 1); + source->ReplaceAllUsesWith(nullptr); + Require(first->Argument(1) == nullptr && target->UseCount() == 4); + Require(source->Opcode() == IrOpcode::IAdd32 && source->Parent() == &graph.block); +} + +void CheckSelfAndEmpty() { + Graph graph(3); + graph.Value(2)->AddArgument(graph.Value(0)); + graph.Value(0)->ReplaceAllUsesWith(graph.Value(0)); + Require(graph.Value(0)->UseCount() == 1 && graph.Value(2)->Argument(0) == graph.Value(0)); + graph.Value(1)->ReplaceAllUsesWith(graph.Value(0)); + Require(graph.Value(0)->UseCount() == 1); + graph.Value(1)->ReplaceAllUsesWith(nullptr); + Require(!graph.Value(1)->HasUses()); +} + +void CheckPhiAndIdentity() { + Graph graph(4); + auto* source = graph.Value(0); + auto* target = graph.Value(1); + auto* phi = graph.Value(2); + phi->ReplaceOpcode(IrOpcode::Phi); + phi->AddPhiOperand(&graph.block, source); + phi->AddPhiOperand(&graph.block, source); + graph.Value(3)->ReplaceOpcode(IrOpcode::Identity); + graph.Value(3)->AddArgument(source); + source->ReplaceUsesWith(target, false); + Require(source->Opcode() == IrOpcode::Unreachable); + Require(phi->Argument(0) == target && phi->Argument(1) == target); + Require(phi->PhiBlockCount() == 2 && phi->PhiBlock(0) == &graph.block && phi->PhiBlock(1) == &graph.block); + Require(graph.Value(3)->Resolve() == target); + Require(target->UseCount() == 3); +} + +void CheckAllocationFailure() { + for (std::ptrdiff_t failure = 0; failure < 2; ++failure) { + Graph graph(12); + for (std::size_t index = 2; index < 12; ++index) { + graph.Value(index)->AddArgument(graph.Value(0)); + } + bool caught = false; + failureAfter = failure; + try { + graph.Value(0)->ReplaceAllUsesWith(graph.Value(1)); + } catch (const std::bad_alloc&) { + caught = true; + } catch (...) { + failureAfter = -1; + throw; + } + failureAfter = -1; + Require(caught && graph.Value(0)->UseCount() == 10); + Require(graph.Value(0)->OperandUses().size() == 10); + Require(!graph.Value(1)->HasUses() && graph.Value(1)->OperandUses().empty()); + for (std::size_t index = 2; index < 12; ++index) { + Require(graph.Value(index)->Argument(0) == graph.Value(0)); + } + graph.Value(0)->ReplaceAllUsesWith(graph.Value(1)); + Require(graph.Value(1)->UseCount() == 10); + } +} + +void CheckGeometricGrowth() { + constexpr std::size_t count = 4096; + Graph graph(count + 2); + std::vector> sources; + for (std::size_t index = 2; index < count + 2; ++index) { + sources.push_back(std::make_unique(IrOpcode::UndefU32, IrType::U32, index + count + 2)); + graph.Value(index)->AddArgument(sources.back().get()); + } + allocations = 0; + countAllocations = true; + for (auto& source : sources) source->ReplaceAllUsesWith(graph.Value(1)); + countAllocations = false; + Require(allocations <= 2 * (std::bit_width(count) + 1)); + Require(graph.Value(1)->UseCount() == count); + for (std::size_t index = 2; index < count + 2; ++index) Require(graph.Value(index)->Argument(0) == graph.Value(1)); +} + +void CheckLargeFanOut() { + constexpr std::size_t count = 100000; + Graph graph(count + 2); + for (std::size_t index = 2; index < count + 2; ++index) { + graph.Value(index)->AddArgument(graph.Value(0)); + graph.Value(index)->AddArgument(graph.Value(0)); + } + graph.Value(0)->ReplaceAllUsesWith(graph.Value(1)); + Require(graph.Value(1)->UseCount() == count * 2 && !graph.Value(0)->HasUses()); + for (std::size_t index = 2; index < count + 2; ++index) { + Require(graph.Value(index)->Argument(0) == graph.Value(1) && graph.Value(index)->Argument(1) == graph.Value(1)); + } +} + +void CheckDifferentialPrograms() { + std::mt19937 random(0x4f23a771); + constexpr std::size_t count = 32; + for (std::size_t trial = 0; trial < 2000; ++trial) { + Graph actual(count), reference(count); + for (std::size_t index = 0; index < count; ++index) { + const auto operands = random() % 5; + for (std::size_t operand = 0; operand < operands; ++operand) { + const auto argument = random() % (count + 1); + actual.Value(index)->AddArgument(argument == count ? nullptr : actual.Value(argument)); + reference.Value(index)->AddArgument(argument == count ? nullptr : reference.Value(argument)); + } + } + for (std::size_t step = 0; step < 24; ++step) { + const auto source = random() % count; + const auto replacement = random() % (count + 1); + if (random() % 4 == 0 && actual.Value(source)->ArgumentCount() > 0) { + const auto operand = random() % actual.Value(source)->ArgumentCount(); + actual.Value(source)->ReplaceArgument(operand, replacement == count ? nullptr : actual.Value(replacement)); + reference.Value(source)->ReplaceArgument(operand, replacement == count ? nullptr : reference.Value(replacement)); + } else { + actual.Value(source)->ReplaceAllUsesWith(replacement == count ? nullptr : actual.Value(replacement)); + ReferenceReplace(reference.Value(source), replacement == count ? nullptr : reference.Value(replacement)); + } + EqualGraphs(actual, reference); + } + } +} + +int main() { + const char* current = "setup"; + try { + const std::pair cases[] = { + {"ordered duplicate operands", CheckSimpleReplacement}, + {"self and empty replacement", CheckSelfAndEmpty}, + {"phi and identity rewriting", CheckPhiAndIdentity}, + {"allocation failure integrity", CheckAllocationFailure}, + {"geometric use-list growth", CheckGeometricGrowth}, + {"large fan-out", CheckLargeFanOut}, + {"differential use graphs", CheckDifferentialPrograms} + }; + for (const auto& [name, test] : cases) { + current = name; + test(); + } + std::puts("7 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons"); + return 0; + } catch (const std::exception& error) { + failureAfter = -1; + countAllocations = false; + std::fprintf(stderr, "%s: %s\n", current, error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp new file mode 100644 index 0000000000..4681d4b223 --- /dev/null +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp @@ -0,0 +1,20 @@ +#include +#include +#include + +namespace IrRewriteAllocation { +std::ptrdiff_t failureAfter = -1; +std::size_t allocations = 0; +bool countAllocations = false; +} + +void* operator new(std::size_t size) { + if (IrRewriteAllocation::failureAfter == 0) throw std::bad_alloc(); + if (IrRewriteAllocation::failureAfter > 0) --IrRewriteAllocation::failureAfter; + if (IrRewriteAllocation::countAllocations) ++IrRewriteAllocation::allocations; + if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} + +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } 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/include/Optimization/SrtWalker/SrtTape.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp new file mode 100644 index 0000000000..0dcb1a168d --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp @@ -0,0 +1,59 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_SRTWALKER_SRTTAPE_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_SRTWALKER_SRTTAPE_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/SrtWalker.hpp" + +#include +#include +#include +#include + +namespace ShaderRecompiler::Detail { + +class SrtTape { +public: + static const SrtTape* For(const IrResourcePlan& plan); + static bool Enabled(); + static bool Verified(); + + bool Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::vector& activeSources) const; + + std::size_t Nodes() const { return nodes.size(); } + +private: + friend class SrtTapeCompiler; + static std::shared_ptr Compile(const IrResourcePlan& plan); + enum class Op : std::uint8_t { + Fail, Const, UserData, ShaderBase, Read, ReadConstBuffer, ExtractHalf, AddCarry, ConstructU64, + ConvertF32U32, ConvertU32F32, FPMul32, FPTrunc32, FPIsNan32, FPOrdLessThanEqual32, FPOrdGreaterThanEqual32, + IAdd32, IAdd64, ISub32, ISub64, IMul32, IMul64, SMin32, UMin32, BitwiseAnd32, BitwiseAnd64, BitwiseOr32, BitwiseXor32, BitwiseNot32, + ShiftLeftLogical32, ShiftLeftLogical64, ShiftRightLogical32, ShiftRightLogical64, ShiftRightArithmetic32, ShiftRightArithmetic64, + BitFieldUExtract, BitFieldSExtract, BitFieldInsert, Select, IEqual32, INotEqual32, SLessThan32, ULessThan32, UGreaterThan32, + LogicalAnd, LogicalOr, LogicalXor, LogicalNot + }; + static constexpr std::uint32_t NoNode = 0xffffffffu; + struct Node { + Op op = Op::Fail; + bool clean = false; + bool loaded = false; + std::uint8_t component = 0; + std::uint8_t argumentCount = 0; + std::int32_t baseWord = -1; + std::array arguments{}; + std::uint64_t immediate = 0; + const IrValue* source = nullptr; + }; + struct Frame; + bool evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) const; + bool compute(Frame& frame, const Node& node, std::uint64_t& result) const; + + std::vector nodes; + std::vector> sourceDwords; + std::vector flatRoots; + std::vector conditionRoots; +}; + +} + +#endif 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..8d78231fb3 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -200,17 +201,25 @@ bool replaceBinaryIdentity(IrValue& inst, IrType type, std::uint64_t identity) { bool foldSelect(IrValue& inst) { auto& condition = resolveArg(inst, 0); - auto& trueValue = resolveArg(inst, 1); - auto& falseValue = resolveArg(inst, 2); if (isImmediate(condition, IrType::U1)) { - replaceWith(inst, condition.ImmediateBool() ? trueValue : falseValue); + replaceWith(inst, resolveArg(inst, condition.ImmediateBool() ? 1 : 2)); return true; } + bool folded = false; + for (std::size_t arm = 1; arm <= 2; arm++) { + auto& nested = resolveArg(inst, arm); + if (nested.Opcode() == inst.Opcode() && nested.ArgumentCount() == 3u && &resolveArg(nested, 0) == &condition) { + inst.ReplaceArgument(arm, &resolveArg(nested, arm)); + folded = true; + } + } + auto& trueValue = resolveArg(inst, 1); + auto& falseValue = resolveArg(inst, 2); if (trueValue == falseValue) { replaceWith(inst, trueValue); return true; } - return false; + return folded; } bool foldPhi(IrValue& inst) { @@ -289,26 +298,25 @@ bool forwardsWord(const IrUse& use) { return use.user->Opcode() == IrOpcode::Identity || use.user->Opcode() == IrOpcode::Phi || (use.user->Opcode() == IrOpcode::SelectU32 && use.operand != 0u); } -void collectReaders(IrValue& value, std::vector& readers, std::vector& visited) { - if (std::ranges::find(visited, &value) != visited.end()) { - return; - } - visited.push_back(&value); - const std::vector uses = value.OperandUses(); - for (const IrUse& use : uses) { - if (forwardsWord(use)) { - collectReaders(*use.user, readers, visited); - } else { +void collectReaders(IrValue& value, std::vector& readers) { + std::unordered_set visited{&value}; + std::vector, std::size_t>> pending; + pending.emplace_back(value.OperandUses(), 0u); + while (!pending.empty()) { + auto& [uses, next] = pending.back(); + if (next == uses.size()) { + pending.pop_back(); + continue; + } + const IrUse use = uses[next++]; + if (!forwardsWord(use)) { readers.push_back(use); + } else if (visited.insert(use.user).second) { + pending.emplace_back(use.user->OperandUses(), 0u); } } } -void collectReaders(IrValue& value, std::vector& readers) { - std::vector visited; - collectReaders(value, readers, visited); -} - constexpr std::array, 2> packedFields {{{8u, 4u}, {16u, 13u}}}; std::optional packedField(std::uint32_t offset, std::uint32_t count) { @@ -846,6 +854,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/DenormalFlushEliminator.cpp b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp index d448da5e6a..f8a013e4b7 100644 --- a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp @@ -19,6 +19,58 @@ const IrValue* maskedOperand(const IrValue* value, IrOpcode opcode, std::uint32_ return nullptr; } +const IrValue* rightOperand(const IrValue* value, IrOpcode opcode, std::uint32_t bits) { + if (value->Opcode() != opcode || value->ArgumentCount() != 2u) return nullptr; + return isImmediate(value->Argument(1)->Resolve(), bits) ? value->Argument(0)->Resolve() : nullptr; +} + +const IrValue* bitsOf(const IrValue* value) { + value = value->Resolve(); + while ((value->Opcode() == IrOpcode::BitCastU32F32 || value->Opcode() == IrOpcode::BitCastF32U32) && value->ArgumentCount() == 1u) value = value->Argument(0)->Resolve(); + return value; +} + +bool testsForNan(const IrValue* condition, const IrValue* bits) { + condition = condition->Resolve(); + if (condition->Opcode() == IrOpcode::FPIsNan32 && condition->ArgumentCount() == 1u) return bitsOf(condition->Argument(0)) == bits; + const IrValue* magnitude = rightOperand(condition, IrOpcode::UGreaterThan32, 0x7f800000u); + if (magnitude != nullptr) magnitude = maskedOperand(magnitude, IrOpcode::BitwiseAnd32, 0x7fffffffu); + return magnitude != nullptr && bitsOf(magnitude) == bits; +} + +bool takenOnlyAsNan(const IrValue* condition, const IrValue* arm) { + const IrValue* bits = bitsOf(arm); + if (const IrValue* quieted = maskedOperand(bits, IrOpcode::BitwiseOr32, 0x00400000u)) bits = bitsOf(quieted); + return testsForNan(condition, bits); +} + +const IrValue* tinyGuardedProduct(const IrValue* condition, const IrValue* arm) { + const IrValue* product = arm->Resolve(); + if (product->Opcode() != IrOpcode::FPMul32 || product->ArgumentCount() != 2u) return nullptr; + const IrValue* offset = rightOperand(condition->Resolve(), IrOpcode::ULessThan32, 0x207fffffu); + const IrValue* magnitude = offset != nullptr ? rightOperand(offset, IrOpcode::ISub32, 1u) : nullptr; + const IrValue* scaled = magnitude != nullptr ? maskedOperand(magnitude, IrOpcode::BitwiseAnd32, 0x7fffffffu) : nullptr; + if (scaled == nullptr) return nullptr; + scaled = bitsOf(scaled); + if (scaled->Opcode() != IrOpcode::FPMul32 || scaled->ArgumentCount() != 2u) return nullptr; + const IrValue* lhs = product->Argument(0)->Resolve(); + const IrValue* rhs = product->Argument(1)->Resolve(); + for (std::size_t index = 0; index < 2u; ++index) { + const IrValue* factor = maskedOperand(scaled->Argument(index)->Resolve(), IrOpcode::FPMul32, 0x5f800000u); + const IrValue* other = scaled->Argument(1u - index)->Resolve(); + if (factor != nullptr && ((factor == lhs && other == rhs) || (factor == rhs && other == lhs))) return product; + } + return nullptr; +} + +bool clearsSignificand(const IrValue& value) { + for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { + const IrValue* mask = value.Argument(index)->Resolve(); + if (mask->HasImmediate() && (mask->ImmediateU32() & 0x007fffffu) == 0u) return true; + } + return false; +} + IrValue* flushedSource(const IrValue& value) { if (value.Opcode() != IrOpcode::SelectU32 || value.ArgumentCount() != 3u) return nullptr; IrValue* source = value.Argument(2)->Resolve(); @@ -65,10 +117,14 @@ private: return false; case IrOpcode::BitwiseAnd32: if (const IrValue* operand = maskedOperand(&value, IrOpcode::BitwiseAnd32, 0x7fffffffu)) return Contains(operand); - return false; + return value.ArgumentCount() == 2u && clearsSignificand(value); case IrOpcode::SelectU32: - case IrOpcode::SelectF32: - return Contains(value.Argument(1)) && Contains(value.Argument(2)); + case IrOpcode::SelectF32: { + const IrValue* condition = value.Argument(0); + const IrValue* product = tinyGuardedProduct(condition, value.Argument(2)); + const bool taken = takenOnlyAsNan(condition, value.Argument(1)) || Contains(value.Argument(1)); + return taken && (product != nullptr ? Contains(product->Argument(0)) && Contains(product->Argument(1)) : Contains(value.Argument(2))); + } case IrOpcode::FPMin32: case IrOpcode::FPMax32: case IrOpcode::FPMinTri32: 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 eb27dd66ea..955aaa1235 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -6,6 +6,7 @@ #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "ImageTableAbi.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -19,12 +20,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include @@ -78,7 +77,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 +259,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)) { @@ -314,6 +312,9 @@ bool TableTraced() { constexpr std::size_t PoisonReasonCount = static_cast(PoisonReason::Count); +enum class NarrowSkip : std::size_t { Value, NotBuffer, KeyScan, Pending, Unmapped, Count }; +constexpr std::array(NarrowSkip::Count)> NarrowSkipNames{"value", "not-buffer", "key-scan", "pending-write", "unmapped"}; + struct TableCounters { std::atomic snapshots{0}; std::atomic tables{0}; @@ -321,6 +322,9 @@ struct TableCounters { std::array, PoisonReasonCount> poison{}; std::atomic narrowed{0}; std::atomic narrowSkipped{0}; + std::array, static_cast(NarrowSkip::Count)> narrowSkips{}; + std::atomic narrowPositions{0}; + std::atomic selected{0}; std::atomic nanoseconds{0}; std::atomic lastReport{0}; }; @@ -348,9 +352,16 @@ void reportTables() { if (count == 0) continue; poison += " " + std::string(ImageTableAbi::PoisonReasonName(static_cast(reason))) + "=" + std::to_string(count); } - std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, structural poison:%s, narrowing %llu applied / %llu skipped, %.1f ms\n", - static_cast(snapshots), static_cast(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast(counters.keys.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(), - static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); + std::string skips; + for (std::size_t reason = 0; reason < counters.narrowSkips.size(); reason++) { + const auto count = counters.narrowSkips[reason].exchange(0, std::memory_order_relaxed); + if (count == 0) continue; + skips += " " + std::string(NarrowSkipNames[reason]) + "=" + std::to_string(count); + if (reason == static_cast(NarrowSkip::KeyScan)) skips += " (up to " + std::to_string(counters.narrowPositions.exchange(0, std::memory_order_relaxed)) + " positions, APS5_IMAGE_TABLE_KEY_SCAN " + std::to_string(TableKeyScanLimit()) + ")"; + } + std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, %llu selected records, structural poison:%s, narrowing %llu applied / %llu skipped%s%s, %.1f ms\n", + static_cast(snapshots), static_cast(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast(counters.keys.exchange(0, std::memory_order_relaxed)), static_cast(counters.selected.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(), + static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), skips.empty() ? "" : ":", skips.c_str(), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried }; @@ -389,6 +400,153 @@ void traceTable(const IrResourcePlan& plan, std::uint32_t table, const TableColu std::fprintf(stderr, "[image-table] shader 0x%llx table %u (%s): base 0x%llx size 0x%llx stride 0x%x addend 0x%x offset 0x%x dwords %u, %u records, %u keys%s, %u structural poison\n", static_cast(plan.shaderHash), table, column.sampler ? "sampler" : "image", static_cast(trace.base), static_cast(trace.size), column.stride, column.addend, column.offset, column.dwordCount, trace.records, trace.keys, trace.narrowed ? " (narrowed)" : "", trace.poison); } +template +struct SlotScratch { + std::vector slots; + std::uint32_t stamp = 0; + bool leased = false; +}; + +template +SlotScratch& threadScratch() { + return HostThreadLocal, TTag>(); +} + +constexpr std::size_t KeptScratchBytes = std::size_t{2} << 20u; + +template +class SlotLease { +public: + explicit SlotLease(SlotScratch& shared) : scratch(shared.leased ? own : shared) { + scratch.leased = true; + Clear(); + } + + SlotLease(const SlotLease&) = delete; + SlotLease& operator=(const SlotLease&) = delete; + + ~SlotLease() { + if (scratch.slots.size() * sizeof(TSlot) > KeptScratchBytes) std::vector().swap(scratch.slots); + scratch.leased = false; + } + + void Clear() { + if (++scratch.stamp != 0u) return; + std::fill(scratch.slots.begin(), scratch.slots.end(), TSlot{}); + scratch.stamp = 1u; + } + + std::vector& Slots() { + return scratch.slots; + } + + const std::vector& Slots() const { + return scratch.slots; + } + + std::uint32_t Stamp() const { + return scratch.stamp; + } + +private: + SlotScratch own; + SlotScratch& scratch; +}; + +struct DenseSlot { + std::uint32_t stamp = 0; + std::uint32_t value = 0; +}; + +class DenseScratchMap { +public: + DenseScratchMap(SlotScratch& shared, std::size_t count) : lease(shared), keys(count) { + if (lease.Slots().size() < keys) lease.Slots().resize(keys); + } + + void Clear() { + lease.Clear(); + } + + std::optional Find(std::size_t key) const { + if (key >= keys) throw std::out_of_range("dense scratch map key is out of range"); + const auto& slot = lease.Slots()[key]; + return slot.stamp == lease.Stamp() ? std::optional(slot.value) : std::nullopt; + } + + void Set(std::size_t key, std::uint32_t value) { + if (key >= keys) throw std::out_of_range("dense scratch map key is out of range"); + lease.Slots()[key] = {lease.Stamp(), value}; + } + +private: + SlotLease lease; + std::size_t keys; +}; + +struct HashSlot { + std::uint64_t hash = 0; + std::uint32_t stamp = 0; + std::uint32_t value = 0; +}; + +std::uint64_t hashWords(std::span words, std::uint64_t seed) { + std::uint64_t hash = 0xcbf29ce484222325ull ^ seed; + for (const auto word : words) hash = (hash ^ word) * 0x100000001b3ull; + hash ^= hash >> 33u; + hash *= 0xff51afd7ed558ccdull; + return hash ^ (hash >> 33u); +} + +class HashScratchIndex { +public: + explicit HashScratchIndex(SlotScratch& shared) : lease(shared) {} + + template + std::optional Find(std::uint64_t hash, TEqual&& equal) const { + const auto& slots = lease.Slots(); + if (slots.empty()) return std::nullopt; + const auto mask = slots.size() - 1u; + for (auto at = static_cast(hash) & mask;; at = (at + 1u) & mask) { + const auto& slot = slots[at]; + if (slot.stamp != lease.Stamp()) return std::nullopt; + if (slot.hash == hash && equal(slot.value)) return slot.value; + } + } + + void Insert(std::uint64_t hash, std::uint32_t value) { + if ((entries + 1u) * 2u > lease.Slots().size()) grow(); + place(lease.Slots(), hash, value); + entries++; + } + +private: + static constexpr std::size_t MinimumSlots = 64; + + void place(std::vector& slots, std::uint64_t hash, std::uint32_t value) const { + const auto mask = slots.size() - 1u; + auto at = static_cast(hash) & mask; + while (slots[at].stamp == lease.Stamp()) at = (at + 1u) & mask; + slots[at] = {hash, lease.Stamp(), value}; + } + + void grow() { + std::vector grown(std::max(MinimumSlots, lease.Slots().size() * 2u)); + for (const auto& slot : lease.Slots()) { + if (slot.stamp == lease.Stamp()) place(grown, slot.hash, slot.value); + } + lease.Slots().swap(grown); + } + + SlotLease lease; + std::size_t entries = 0; +}; + +struct WordsIndexTag {}; +struct ElementIndexTag {}; +struct PoisonIndexTag {}; +struct TableRecordsTag {}; + class TableSnapshotter { public: TableSnapshotter(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, const std::vector& activeSources, ImageTableSnapshot& tables) : plan(plan), runtime(runtime), walker(walker), activeSources(activeSources), tables(tables) {} @@ -472,8 +630,10 @@ private: } void Narrow(const TableColumn& column, ColumnRecords& records) { - const auto skip = [&] { - tableCounters().narrowSkipped.fetch_add(1, std::memory_order_relaxed); + const auto skip = [&](NarrowSkip reason) { + auto& counters = tableCounters(); + counters.narrowSkipped.fetch_add(1, std::memory_order_relaxed); + counters.narrowSkips[static_cast(reason)].fetch_add(1, std::memory_order_relaxed); }; std::vector values{0u}; std::vector> scanned; @@ -483,7 +643,7 @@ private: try { walker.EvaluateDescriptorSource(plan, domain.source, runtime, key); } catch (const std::exception&) { - skip(); + skip(NarrowSkip::Value); return; } values.push_back(key.dwords[0]); @@ -491,7 +651,7 @@ private: } const auto keyBuffer = EvaluateBuffer(domain.source); if (keyBuffer.Type() != 0u) { - skip(); + skip(NarrowSkip::NotBuffer); return; } const auto base = keyBuffer.Base48(); @@ -499,44 +659,112 @@ private: const std::uint64_t first = domain.offset & ~3u; const std::uint64_t positions = size >= first + 4u ? (size - first - 4u) / domain.stride + 1u : 0u; if (positions > TableKeyScanLimit() || first > 0xffffffffull) { - skip(); + auto& widest = tableCounters().narrowPositions; + auto seen = widest.load(std::memory_order_relaxed); + while (seen < positions && !widest.compare_exchange_weak(seen, positions, std::memory_order_relaxed)) {} + skip(NarrowSkip::KeyScan); return; } if (positions != 0u && runtime.pendingWrite != nullptr && runtime.pendingWrite(runtime.userContext, base + first, size - first)) { - skip(); + 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(); - 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); tableCounters().narrowed.fetch_add(1, std::memory_order_relaxed); } + void Select(const TableColumn& column, ColumnRecords& records) { + const auto base = records.base; + const auto size = records.size; + DescriptorValue selection; + try { + walker.EvaluateDescriptorSource(plan, column.selection, runtime, selection); + } catch (const std::exception&) { + records.fault = ImageTableAbi::PoisonCode(static_cast(PoisonReason::Unmapped)); + return; + } + tableCounters().selected.fetch_add(1, std::memory_order_relaxed); + const auto offset = selection.dwords[0]; + const auto entry = (column.addend + column.offset) & ~3u; + records.base = base + ((offset + column.offset) & ~3u) - entry; + records.size = entry + static_cast(column.dwordCount) * sizeof(std::uint32_t); + records.records = 1u; + records.keys = 1u; + records.states.assign(1u, RecordState::Read); + records.words.assign(1u, 0u); + if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) { + records.states.front() = RecordState::Carried; + return; + } + DescriptorValue value; + value.dwordCount = 8u; + std::uint64_t low = std::numeric_limits::max(); + std::uint64_t high = 0; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + const auto position = ImageTableAbi::ScalarBufferDword(offset, column.offset + dword * 4u, size); + if (!position.has_value()) { + value.availableDwordMask |= static_cast(1u << dword); + continue; + } + const auto address = base + *position; + if (!PageAccessible(address, base, base + size)) { + records.states.front() = RecordState::Unmapped; + return; + } + value.dwords[dword] = Read(address); + value.availableDwordMask |= static_cast(1u << dword); + low = std::min(low, address); + high = std::max(high, address + sizeof(std::uint32_t)); + } + records.words.front() = WordsIndex(value); + if (low < high) tables.ranges.emplace_back(low, high - low); + } + std::uint32_t WordsIndex(const DescriptorValue& value) { - const auto [found, inserted] = wordsIndex.try_emplace({value.dwords, value.dwordCount, value.availableDwordMask}, static_cast(tables.words.size())); - if (inserted) tables.words.push_back(value); - return found->second; + const auto hash = hashWords(value.dwords, (static_cast(value.dwordCount) << 8u) | value.availableDwordMask); + if (const auto found = wordsIndex.Find(hash, [&](std::uint32_t index) { return tables.words[index] == value; })) return *found; + const auto index = static_cast(tables.words.size()); + tables.words.push_back(value); + wordsIndex.Insert(hash, index); + return index; } ColumnRecords& Column(std::uint32_t source) { @@ -563,6 +791,10 @@ private: } records.base = heap.Base48(); records.size = heap.GetSize(); + if (column.selected) { + Select(column, records); + return records; + } records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset + (column.indirect ? 4u : 0u), records.size); if (records.size != 0u) tables.ranges.emplace_back(records.base, records.size); } @@ -764,17 +996,10 @@ private: const std::vector& activeSources; ImageTableSnapshot& tables; std::map columns; - struct WordsHash { - using Key = std::tuple, std::uint32_t, std::uint8_t>; - std::size_t operator()(const Key& key) const noexcept { - std::uint64_t hash = 0xcbf29ce484222325ull ^ std::get<1>(key) ^ std::get<2>(key); - for (const auto word : std::get<0>(key)) hash = (hash ^ word) * 0x100000001b3ull; - return static_cast(hash ^ (hash >> 32u)); - } - }; - std::unordered_map wordsIndex; + HashScratchIndex wordsIndex{threadScratch()}; 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) { @@ -1070,16 +1295,29 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i out.map[Abi::HeaderVersion] = Abi::Version; out.poison.push_back({{}, 0u, NoTable, PoisonReason::OutsideSnapshot}); out.poison.push_back({{}, 0u, NoTable, PoisonReason::OutsideDomain}); - std::map, std::uint32_t>, std::uint32_t> poisonIndex; - std::set expected; + HashScratchIndex poisonIndex{threadScratch()}; + std::vector expected; + std::size_t expectedCount = 0; const auto poison = [&](std::uint32_t resource, PoisonReason reason, const DescriptorValue* words) { const auto value = words != nullptr ? *words : DescriptorValue{}; - const auto [found, inserted] = poisonIndex.try_emplace({resource, reason, value.dwords, value.dwordCount}, static_cast(out.poison.size())); - if (inserted) out.poison.push_back({value.dwords, value.dwordCount, resource, reason}); - expected.insert(found->second); - return Abi::PoisonCode(found->second); + const auto hash = hashWords(value.dwords, (static_cast(resource) << 40u) ^ (static_cast(reason) << 8u) ^ value.dwordCount); + auto index = poisonIndex.Find(hash, [&](std::uint32_t known) { + const auto& record = out.poison[known]; + return record.resource == resource && record.reason == reason && record.words == value.dwords && record.dwordCount == value.dwordCount; + }); + if (!index.has_value()) { + index = static_cast(out.poison.size()); + out.poison.push_back({value.dwords, value.dwordCount, resource, reason}); + poisonIndex.Insert(hash, *index); + } + if (*index >= expected.size()) expected.resize(out.poison.size(), false); + if (!expected[*index]) { + expected[*index] = true; + expectedCount++; + } + return Abi::PoisonCode(*index); }; - std::map, RdnaImageDimension, bool, DescriptorBindingKind>, std::uint32_t> elementIndex; + HashScratchIndex elementIndex{threadScratch()}; std::vector directSamplers(info.images.size(), 0u); std::vector reducingSampler(info.images.size(), false); for (const auto& pair : info.sampledPairs) { @@ -1112,15 +1350,22 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i auto element = words.dwords; if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; const auto kind = DescriptorBindingForImage(mode); - const auto [found, inserted] = elementIndex.try_emplace({element, mode.dimension, mode.depthCompare, kind}, static_cast(out.elements.size())); - if (inserted && out.elements.size() >= Abi::ImageElementBudget) { - elementIndex.erase(found); + const auto hash = hashWords(element, (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); + auto index = elementIndex.Find(hash, [&](std::uint32_t known) { + const auto& entry = out.elements[known]; + return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind; + }); + if (!index.has_value() && out.elements.size() >= Abi::ImageElementBudget) { record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { - if (inserted) out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u}); - out.elements[found->second].samplers |= directSamplers[resource]; + if (!index.has_value()) { + index = static_cast(out.elements.size()); + out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u}); + elementIndex.Insert(hash, *index); + } + out.elements[*index].samplers |= directSamplers[resource]; record[Abi::ImageRecordFlags] = decoded << Abi::ModeShift; - record[Abi::ImageRecordElement] = found->second; + record[Abi::ImageRecordElement] = *index; } } } @@ -1154,6 +1399,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i return offset; }; std::vector outside(count, false); + DenseScratchMap records(threadScratch(), tables.words.size()); const auto fill = [&](std::uint32_t resource, std::uint32_t table, bool sampler) { const auto& column = tables.tables.at(table); const auto header = Abi::TableHeader(table); @@ -1172,7 +1418,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i const auto mapStart = static_cast(out.map.size()); out.map[header + Abi::TableMapStart] = mapStart; out.map.resize(out.map.size() + column.keys, 0u); - std::unordered_map records; + records.Clear(); for (std::uint32_t key = 0; key < column.keys; key++) { const auto code = column.codes[key]; std::uint32_t mapped = Abi::NullCode; @@ -1180,11 +1426,12 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i const auto reason = static_cast(code & ~Abi::PoisonFlag); mapped = reason == PoisonReason::OutsideDomain ? Abi::PoisonCode(Abi::OutsideDomainPoison) : poison(resource, reason, nullptr); } else if (out.map[header + Abi::TableFault] == 0u) { - const auto& words = tables.words.at(code); - if (sampler || (words.availableDwordMask & (1u << 3u)) == 0u || !nullImageDescriptor(info.images[resource], words)) { - const auto [found, inserted] = records.try_emplace(code, 0u); - if (inserted) found->second = sampler ? samplerRecord(resource, words) : imageRecord(resource, words); - mapped = found->second; + if (const auto known = records.Find(code)) { + mapped = *known; + } else { + const auto& words = tables.words[code]; + if (sampler || (words.availableDwordMask & (1u << 3u)) == 0u || !nullImageDescriptor(info.images[resource], words)) mapped = sampler ? samplerRecord(resource, words) : imageRecord(resource, words); + records.Set(code, mapped); } } out.map[mapStart + key] = mapped; @@ -1209,7 +1456,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i out.map.resize(out.map.size() + out.elements.size(), 0u); out.map[Abi::HeaderSamplerFaults] = static_cast(out.map.size()); out.map.resize(out.map.size() + out.samplers.size() / Abi::SamplerEntryElements, 0u); - out.faults = static_cast(expected.size() + static_cast(std::count(outside.begin(), outside.end(), true))); + out.faults = static_cast(expectedCount + static_cast(std::count(outside.begin(), outside.end(), true))); return out; } @@ -1261,7 +1508,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); @@ -1318,7 +1565,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; @@ -1503,7 +1750,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 82f21af388..73b74dad2a 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -146,11 +146,15 @@ public: } std::vector referenced; for (const auto& plan : m_tableColumns) { - for (std::uint32_t argument = 0; argument < plan.handle->ArgumentCount(); argument++) { - plan.handle->ReplaceArgument(argument, plan.offset); - } const auto& column = *m_sources[plan.source].tableColumn; + IrValue* offset = column.selected ? &m_builder.Constant(column.addend) : plan.offset; + for (std::uint32_t argument = 0; argument < plan.handle->ArgumentCount(); argument++) { + plan.handle->ReplaceArgument(argument, offset); + } KeepSourceAlive(*plan.handle, m_sources[column.heapSource], referenced); + if (column.selected) { + KeepSourceAlive(*plan.handle, m_sources[column.selection], referenced); + } if (plan.keyDomain.has_value()) { KeepSourceAlive(*plan.handle, m_sources[plan.keyDomain->source], referenced); } @@ -209,6 +213,9 @@ private: IrValue* canonicalizeSampleAdjustDword3(IrValue* value) { for (;;) { value = value->Resolve(); + if ((possibleU32Bits(value) & ~samplerDword3ReservedMask) == 0u) { + return &m_builder.Constant(0u); + } if (value->Opcode() != IrOpcode::BitwiseOr32) { return value; } @@ -216,9 +223,6 @@ private: IrValue* right = value->Argument(1)->Resolve(); const bool leftReserved = (possibleU32Bits(left) & ~samplerDword3ReservedMask) == 0u; const bool rightReserved = (possibleU32Bits(right) & ~samplerDword3ReservedMask) == 0u; - if (leftReserved && rightReserved) { - return &m_builder.Constant(0u); - } if (leftReserved) { value = right; } else if (rightReserved) { @@ -542,11 +546,15 @@ private: static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) { value = value->Resolve(); addend = 0; - if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { - if (immediateU32(value->Argument(0), addend)) return value->Argument(1)->Resolve(); - if (immediateU32(value->Argument(1), addend)) return value->Argument(0)->Resolve(); + while (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + std::uint32_t immediate = 0; + IrValue* rest = nullptr; + if (immediateU32(value->Argument(0), immediate)) rest = value->Argument(1)->Resolve(); + else if (immediateU32(value->Argument(1), immediate)) rest = value->Argument(0)->Resolve(); + else break; + addend += immediate; + value = rest; } - addend = 0; return value; } @@ -650,6 +658,18 @@ private: column.address = address; column.foldedAddend = folded ? baseAddend : 0u; column.foldedCarry = folded ? carry : 0u; + if (!address && !sampler) { + DescriptorSource direct; + MakeSource(handle, width, false, false, direct); + DescriptorSource selection; + selection.dwordCount = 1u; + selection.dwords[0] = offset; + std::uint32_t badDword = 0; + if (ValidateSource(direct, badDword) && ValidateSource(selection, badDword)) { + column.selected = true; + column.selection = InternSource(selection); + } + } const auto domain = masked ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; columnSource.tableColumn = column; @@ -1273,6 +1293,7 @@ private: return; } IndexMemoryUsers(); + const auto directImages = DirectImageHandles(); for (auto& block : m_program.Blocks()) { for (IrValue* inst : block->Instructions()) { const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); @@ -1284,7 +1305,10 @@ private: continue; } const auto memory = m_program.Resources().memoryInfo[flags.index]; - static_cast(PlanTableColumn(*inst->Argument(0)->Resolve(), false, memory.imageR128)); + IrValue& image = *inst->Argument(0)->Resolve(); + if (std::ranges::none_of(directImages, [&](const IrValue* handle) { return SameHandle(*handle, image); })) { + static_cast(PlanTableColumn(image, false, memory.imageR128)); + } if (imageInfo.needsSampler && inst->ArgumentCount() > 1u && !DirectSampler(*inst->Argument(1)->Resolve())) { static_cast(PlanTableColumn(*inst->Argument(1)->Resolve(), true, false)); } @@ -1328,6 +1352,54 @@ private: return ValidateSource(descriptor, badDword); } + bool DirectImage(const IrValue& handle) { + if (handle.Opcode() != IrOpcode::GetImageResource || handle.ArgumentCount() != 8u) { + return false; + } + DescriptorSource descriptor; + MakeSource(handle, 8u, false, false, descriptor); + std::uint32_t badDword = 0; + return ValidateSource(descriptor, badDword); + } + + std::vector DirectImageHandles() { + std::vector handles; + for (auto& block : m_program.Blocks()) { + for (IrValue* inst : block->Instructions()) { + const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); + if (imageInfo.access == ImageAccess::None || inst->ArgumentCount() == 0u) { + continue; + } + const auto flags = inst->Flags(); + if (flags.index >= m_program.Resources().memoryInfo.size()) { + continue; + } + const auto& memory = m_program.Resources().memoryInfo[flags.index]; + const bool tableUnsupported = imageInfo.resourceClass == ImageResourceClass::Storage || memory.imagePacked || memory.imageByElements != 0u; + const IrValue* handle = inst->Argument(0)->Resolve(); + if (tableUnsupported && DirectImage(*handle)) { + handles.push_back(handle); + } + } + } + return handles; + } + + bool SameHandle(const IrValue& left, const IrValue& right) const { + if (&left == &right) { + return true; + } + if (left.Opcode() != right.Opcode() || left.ArgumentCount() != right.ArgumentCount()) { + return false; + } + for (std::size_t index = 0; index < left.ArgumentCount(); index++) { + if (!EquivalentValue(m_program.Resources(), left.Argument(index), right.Argument(index))) { + return false; + } + } + return true; + } + bool IsTablePlanningMemory(std::uint32_t index) const { return std::ranges::find(m_tablePlanningMemory, index) != m_tablePlanningMemory.end(); } @@ -1336,8 +1408,59 @@ private: return source < m_sources.size() && m_sources[source].tableColumn.has_value(); } + bool SameBufferSource(const IrValue& handle, std::uint32_t source) const { + const auto& current = m_sources[source]; + if (current.tableColumn.has_value() || current.dwordCount != 4u || handle.ArgumentCount() != 4u) { + return false; + } + for (std::uint32_t dword = 0; dword < 4u; dword++) { + if (!EquivalentValue(m_program.Resources(), current.dwords[dword], handle.Argument(dword))) { + return false; + } + } + return true; + } + + bool SelectionWritten(const TableColumn& column) const { + const auto written = [&](const auto& matches) { + return std::any_of(m_info.buffers.begin(), m_info.buffers.end(), [&](const auto& buffer) { return (buffer.written || buffer.atomic) && matches(buffer.source); }); + }; + std::vector pending{m_sources[column.selection].dwords[0]}; + std::vector seen; + while (!pending.empty()) { + const IrValue* current = pending.back()->Resolve(); + pending.pop_back(); + if (std::ranges::find(seen, current) != seen.end()) { + continue; + } + seen.push_back(current); + const bool pointer = current->Opcode() == IrOpcode::LoadAddressU32; + const bool buffer = current->Opcode() == IrOpcode::ReadConstBuffer && current->ArgumentCount() != 0u; + if ((pointer || buffer) && m_info.bdaWrites) { + return true; + } + if (pointer && written([](std::uint32_t) { return true; })) { + return true; + } + if (buffer) { + const IrValue* handle = current->Argument(0)->Resolve(); + if (written([&](std::uint32_t source) { return SameBufferSource(*handle, source); })) { + return true; + } + } + for (std::size_t index = 0; index < current->ArgumentCount(); index++) { + pending.push_back(current->Argument(index)); + } + } + return false; + } + void CheckTableStores() { for (auto& source : m_sources) { + if (source.tableColumn.has_value() && source.tableColumn->selected && SelectionWritten(*source.tableColumn)) { + source.tableColumn->selected = false; + source.tableColumn->keyDomains.clear(); + } if (!source.tableColumn.has_value() || source.tableColumn->keyDomains.empty()) { continue; } @@ -1358,8 +1481,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; } } @@ -1514,7 +1636,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/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 19c4663e86..c83724c373 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -1,10 +1,13 @@ #include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" +#include "Optimization/SrtWalker/SrtTape.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include #include #include +#include +#include #include #include #include @@ -226,9 +229,7 @@ std::string describeDifference(const std::vector& compactResult return "sizes " + std::to_string(compactResults.size()) + "/" + std::to_string(irResults.size()) + " sources, " + std::to_string(compactFlat.size()) + "/" + std::to_string(irFlat.size()) + " flat words"; } -} - -bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { +bool interpretRuntimeSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { if (program.compact.values.empty()) return evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); prefetchCompactPlan(program.compact); const bool evaluated = evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); @@ -250,6 +251,96 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::spanbegin(), reads->end()); + reads->erase(std::unique(reads->begin(), reads->end()), reads->end()); + } + return tapeReads == interpretedReads; +} + +void verifyTape(const IrResourcePlan& program, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads, std::size_t leavesBefore, std::size_t readsBefore, const SrtUserDataUse* useBefore) { + SrtReadTrace expectedTrace; + SrtUserDataUse expectedUse = useBefore != nullptr ? *useBefore : SrtUserDataUse {}; + SrtRuntime interpreted = runtime; + interpreted.readTrace = runtime.readTrace != nullptr ? &expectedTrace : nullptr; + interpreted.userDataUse = runtime.userDataUse != nullptr ? &expectedUse : nullptr; + std::vector expectedResults; + std::vector expectedFlat; + std::vector expectedActive; + std::vector expectedPoison; + std::uint32_t expectedNullRoots = 0; + const bool ok = interpretRuntimeSources(program, program.materializationSources, interpreted, expectedResults, expectedFlat, true, program.cleanFlatSlots, expectedActive, &expectedPoison, &expectedNullRoots); + std::string difference; + if (!ok) difference = "the Evaluator failed: " + failureReason(); + else if (expectedResults != results) difference = "descriptors"; + else if (expectedFlat != flat) difference = "flat slots"; + else if (expectedActive != activeSources) difference = "active sources"; + else if (!expectedPoison.empty() || expectedNullRoots != 0u) difference = "poisoned reads"; + else if (auto* trace = runtime.readTrace; trace != nullptr) { + auto leaves = std::vector(trace->leaves.begin() + static_cast(leavesBefore), trace->leaves.end()); + auto reads = std::vector(trace->otherReads.begin() + static_cast(readsBefore), trace->otherReads.end()); + auto expectedLeaves = expectedTrace.leaves; + auto expectedReads = expectedTrace.otherReads; + std::sort(leaves.begin(), leaves.end()); + std::sort(expectedLeaves.begin(), expectedLeaves.end()); + std::sort(reads.begin(), reads.end()); + reads.erase(std::unique(reads.begin(), reads.end()), reads.end()); + std::sort(expectedReads.begin(), expectedReads.end()); + expectedReads.erase(std::unique(expectedReads.begin(), expectedReads.end()), expectedReads.end()); + if (leaves != expectedLeaves) difference = "traced leaf reads"; + else if (reads != expectedReads) difference = "traced reads"; + if (!difference.empty()) { + trace->leaves.resize(leavesBefore); + trace->otherReads.resize(readsBefore); + trace->leaves.insert(trace->leaves.end(), expectedTrace.leaves.begin(), expectedTrace.leaves.end()); + trace->otherReads.insert(trace->otherReads.end(), expectedTrace.otherReads.begin(), expectedTrace.otherReads.end()); + } + } + if (difference.empty() && runtime.userDataUse != nullptr && !sameUserDataUse(*runtime.userDataUse, expectedUse)) difference = "user-data use"; + static std::atomic walks {0}; + static std::atomic differences {0}; + const auto walked = walks.fetch_add(1, std::memory_order_relaxed) + 1; + if (difference.empty()) { + bool decade = walked >= 10; + for (auto rest = walked; decade && rest >= 10; rest /= 10) decade = rest % 10 == 0; + if (decade) std::fprintf(stderr, "[srt-tape] verify: %llu walks, %llu differed\n", static_cast(walked), static_cast(differences.load(std::memory_order_relaxed))); + return; + } + if (differences.fetch_add(1, std::memory_order_relaxed) < 20) std::fprintf(stderr, "[srt-tape] verify: shader %016llx: the tape's walk differs from the Evaluator's in its %s\n", static_cast(program.shaderHash), difference.c_str()); + if (!ok) return; + if (runtime.userDataUse != nullptr) *runtime.userDataUse = std::move(expectedUse); + results = std::move(expectedResults); + flat = std::move(expectedFlat); + activeSources = std::move(expectedActive); + if (poison != nullptr) *poison = std::move(expectedPoison); + if (nullRootReads != nullptr) *nullRootReads = expectedNullRoots; +} + +} + +bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { + const bool planInputs = evaluateFlat && sources.data() == program.materializationSources.data() && sources.size() == program.materializationSources.size() && cleanFlatSlots.data() == program.cleanFlatSlots.data() && cleanFlatSlots.size() == program.cleanFlatSlots.size(); + if (planInputs) { + if (const auto* tape = SrtTape::For(program); tape != nullptr) { + const auto leavesBefore = runtime.readTrace != nullptr ? runtime.readTrace->leaves.size() : 0; + const auto readsBefore = runtime.readTrace != nullptr ? runtime.readTrace->otherReads.size() : 0; + std::optional useBefore; + if (SrtTape::Verified() && runtime.userDataUse != nullptr) useBefore = *runtime.userDataUse; + if (tape->Run(program, runtime, results, flat, activeSources)) { + if (poison != nullptr) poison->clear(); + if (nullRootReads != nullptr) *nullRootReads = 0; + if (SrtTape::Verified()) verifyTape(program, runtime, results, flat, activeSources, poison, nullRootReads, leavesBefore, readsBefore, useBefore ? &*useBefore : nullptr); + return true; + } + } + } + return interpretRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); +} + const std::string& RuntimeSourceFailureReason() { return failureReason(); } 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/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp new file mode 100644 index 0000000000..5bb86efe50 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -0,0 +1,621 @@ +#include "Optimization/SrtWalker/SrtTape.hpp" +#include "Optimization/SrtWalker/SrtAddressArithmetic.hpp" +#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler::Detail { + +namespace { + +float float32(std::uint64_t bits) { return std::bit_cast(static_cast(bits)); } + +std::uint64_t float32Bits(float value) { return std::bit_cast(value); } + +constexpr std::uint32_t MainContext = 0; +constexpr std::uint32_t CleanContext = 1; +constexpr std::uint32_t MaxDepth = 512; + +} + +class SrtTapeCompiler { +public: + SrtTapeCompiler(const IrResourcePlan& plan, SrtTape& tape) : plan(plan), tape(tape) { + contexts.push_back({nullptr, false, true}); + contexts.push_back({nullptr, true, false}); + } + + bool Build() { + tape.sourceDwords.assign(plan.descriptorSources.size(), {}); + for (auto& dwords : tape.sourceDwords) dwords.fill(SrtTape::NoNode); + for (const auto index : plan.materializationSources) { + if (index >= plan.descriptorSources.size()) return false; + const auto& source = plan.descriptorSources[index]; + if (source.dwordCount > source.dwords.size()) return false; + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) tape.sourceDwords[index][dword] = compile(source.dwords[dword], MainContext, 0); + } + tape.flatRoots.resize(plan.srtReads.size()); + for (std::size_t slot = 0; slot < plan.srtReads.size(); ++slot) { + const auto& read = plan.srtReads[slot]; + const bool clean = read.flatOffset < plan.cleanFlatSlots.size() && plan.cleanFlatSlots[read.flatOffset] != 0u; + tape.flatRoots[slot] = compile(read.value, clean ? CleanContext : MainContext, 0); + } + tape.conditionRoots.assign(plan.controlFlow.size(), SrtTape::NoNode); + for (std::size_t block = 0; block < plan.controlFlow.size(); ++block) { + if (plan.controlFlow[block].condition != nullptr) tape.conditionRoots[block] = compile(plan.controlFlow[block].condition, CleanContext, 0); + } + return !malformed; + } + +private: + using Op = SrtTape::Op; + using Node = SrtTape::Node; + struct Context { + const IrValue* mask; + bool clean; + bool cleanSlots; + }; + static constexpr std::uint32_t Building = 0xfffffffeu; + + std::uint32_t emit(const Node& node) { + tape.nodes.push_back(node); + return static_cast(tape.nodes.size() - 1); + } + + std::uint32_t fail() { + if (failNode == SrtTape::NoNode) failNode = emit({}); + return failNode; + } + + std::uint32_t malformedFail() { + malformed = true; + return fail(); + } + + std::uint32_t constant(std::uint64_t value) { + Node node; + node.op = Op::Const; + node.immediate = value; + return emit(node); + } + + std::uint32_t compile(IrValue* raw, std::uint32_t context, std::uint32_t depth) { + if (raw == nullptr || depth > MaxDepth) return malformedFail(); + IrValue* value = raw->Resolve(); + if (value->HasImmediate()) { + switch (value->Type()) { + case IrType::Bool: return constant(value->ImmediateBool() ? 1u : 0u); + case IrType::U8: return constant(value->ImmediateU8()); + case IrType::U16: return constant(value->ImmediateU16()); + case IrType::U32: return constant(value->ImmediateU32()); + case IrType::U64: return constant(value->ImmediateU64()); + case IrType::F32: return constant(float32Bits(value->ImmediateF32())); + default: return fail(); + } + } + if (value->Opcode() == IrOpcode::Void) return fail(); + const auto mask = contexts[context].mask; + if (mask != nullptr && IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3 && value->Argument(0)->Resolve() == mask) return compile(value->Argument(1), context, depth + 1); + const auto key = std::make_pair(static_cast(value), context); + if (const auto found = built.find(key); found != built.end()) return found->second == Building ? fail() : found->second; + built.emplace(key, Building); + const auto index = build(value, context, depth); + built[key] = index; + return index; + } + + std::uint32_t operation(Op op, IrValue* value, std::uint32_t context, std::uint32_t depth, std::uint8_t count) { + if (value->ArgumentCount() < count) return malformedFail(); + Node node; + node.op = op; + node.argumentCount = count; + for (std::uint8_t index = 0; index < count; ++index) node.arguments[index] = compile(value->Argument(index), context, depth + 1); + return emit(node); + } + + std::uint32_t build(IrValue* value, std::uint32_t context, std::uint32_t depth) { + const auto current = contexts[context]; + switch (value->Opcode()) { + case IrOpcode::GetUserData: { + if (value->ArgumentCount() < 1 || value->Argument(0) == nullptr) return malformedFail(); + const auto reg = RegIndex(static_cast(value->Argument(0)->Register().index)); + if (reg < plan.userDataBase) return fail(); + Node node; + node.op = Op::UserData; + node.immediate = reg - plan.userDataBase; + return emit(node); + } + case IrOpcode::GetShaderBase: { + Node node; + node.op = Op::ShaderBase; + return emit(node); + } + case IrOpcode::Phi: { + IrValue* resolved = ResolveInvariantPhi(plan, value); + return resolved != nullptr ? compile(resolved, context, depth + 1) : fail(); + } + case IrOpcode::ReadFirstLane: { + if (value->ArgumentCount() < 2) return malformedFail(); + IrValue* laneMask = value->Argument(1); + contexts.push_back({laneMask != nullptr ? laneMask->Resolve() : nullptr, current.clean, current.cleanSlots}); + return compile(value->Argument(0), static_cast(contexts.size() - 1), depth + 1); + } + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + if (value->ArgumentCount() < 1) return malformedFail(); + return compile(value->Argument(0), context, depth + 1); + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: { + if (value->ArgumentCount() < 2) return malformedFail(); + IrValue* index = value->Argument(1)->Resolve(); + if (!index->HasImmediate() || index->Type() != IrType::U32) return fail(); + const auto component = index->ImmediateU32(); + if (component >= 2u) return fail(); + if (value->Opcode() == IrOpcode::CompositeExtractU64) { + Node node; + node.op = Op::ExtractHalf; + node.component = static_cast(component); + node.argumentCount = 1; + node.arguments[0] = compile(value->Argument(0), context, depth + 1); + return emit(node); + } + IrValue* source = value->Argument(0)->Resolve(); + if (source->Opcode() == IrOpcode::Void) return fail(); + if (source->Opcode() == IrOpcode::CompositeConstructU32x2) { + if (source->ArgumentCount() <= component) return malformedFail(); + return compile(source->Argument(component), context, depth + 1); + } + if (source->Opcode() == IrOpcode::IAddCarry32) { + if (source->ArgumentCount() < 2) return malformedFail(); + Node node; + node.op = Op::AddCarry; + node.component = static_cast(component); + node.argumentCount = 2; + node.arguments[0] = compile(source->Argument(0), context, depth + 1); + node.arguments[1] = compile(source->Argument(1), context, depth + 1); + return emit(node); + } + return fail(); + } + case IrOpcode::CompositeConstructU64: return operation(Op::ConstructU64, value, context, depth, 2); + case IrOpcode::ReadConst: { + if (value->ArgumentCount() < 2) return malformedFail(); + IrValue* slot = value->Argument(1)->Resolve(); + if (!slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= plan.srtReads.size()) return fail(); + const auto index = slot->ImmediateU32(); + const bool clean = current.cleanSlots && index < plan.cleanFlatSlots.size() && plan.cleanFlatSlots[index] != 0u; + return compile(plan.srtReads[index].value, clean ? CleanContext : context, depth + 1); + } + case IrOpcode::LoadAddressU32: + case IrOpcode::ReadConstBuffer: { + if (!IsRawRead(plan, *value)) return fail(); + if (value->ArgumentCount() < 2) return malformedFail(); + if (value->Flags().index >= plan.memoryInfo.size()) return fail(); + const auto& memory = plan.memoryInfo[value->Flags().index]; + IrValue* handle = value->Argument(0)->Resolve(); + if (handle->Opcode() == IrOpcode::Void) return fail(); + if (handle->ArgumentCount() < 2) return malformedFail(); + Node node; + node.op = value->Opcode() == IrOpcode::ReadConstBuffer ? Op::ReadConstBuffer : Op::Read; + node.clean = current.clean; + node.loaded = LoadedFromMemory(*handle); + node.source = value; + if (IrValue* low = handle->Argument(0); low != nullptr) { + low = low->Resolve(); + if (!low->HasImmediate() && low->Opcode() == IrOpcode::GetUserData && low->ArgumentCount() >= 1 && low->Argument(0) != nullptr) { + const auto reg = RegIndex(static_cast(low->Argument(0)->Register().index)); + if (reg >= plan.userDataBase && reg - plan.userDataBase < SrtUserDataUse::TrackedWords) node.baseWord = static_cast(reg - plan.userDataBase); + } + } + const auto immediate = static_cast(static_cast(memory.offset)); + node.immediate = static_cast(immediate); + node.arguments[0] = compile(handle->Argument(0), context, depth + 1); + node.arguments[1] = compile(handle->Argument(1), context, depth + 1); + node.arguments[2] = compile(value->Argument(1), context, depth + 1); + node.argumentCount = 3; + if (node.op == Op::ReadConstBuffer) { + if (handle->ArgumentCount() != 4u || immediate < 0) return fail(); + node.arguments[3] = compile(handle->Argument(2), context, depth + 1); + node.arguments[4] = compile(handle->Argument(3), context, depth + 1); + node.argumentCount = 5; + } + return emit(node); + } + case IrOpcode::IAdd32: return operation(Op::IAdd32, value, context, depth, 2); + case IrOpcode::IAdd64: return operation(Op::IAdd64, value, context, depth, 2); + case IrOpcode::ISub32: return operation(Op::ISub32, value, context, depth, 2); + case IrOpcode::ISub64: return operation(Op::ISub64, value, context, depth, 2); + case IrOpcode::IMul32: return operation(Op::IMul32, value, context, depth, 2); + case IrOpcode::IMul64: return operation(Op::IMul64, value, context, depth, 2); + case IrOpcode::SMin32: return operation(Op::SMin32, value, context, depth, 2); + case IrOpcode::UMin32: return operation(Op::UMin32, value, context, depth, 2); + case IrOpcode::ConvertF32U32: return operation(Op::ConvertF32U32, value, context, depth, 1); + case IrOpcode::ConvertU32F32: return operation(Op::ConvertU32F32, value, context, depth, 1); + case IrOpcode::FPMul32: return operation(Op::FPMul32, value, context, depth, 2); + case IrOpcode::FPTrunc32: return operation(Op::FPTrunc32, value, context, depth, 1); + case IrOpcode::FPIsNan32: return operation(Op::FPIsNan32, value, context, depth, 1); + case IrOpcode::FPOrdLessThanEqual32: return operation(Op::FPOrdLessThanEqual32, value, context, depth, 2); + case IrOpcode::FPOrdGreaterThanEqual32: return operation(Op::FPOrdGreaterThanEqual32, value, context, depth, 2); + case IrOpcode::BitwiseAnd32: return operation(Op::BitwiseAnd32, value, context, depth, 2); + case IrOpcode::BitwiseAnd64: return operation(Op::BitwiseAnd64, value, context, depth, 2); + case IrOpcode::BitwiseOr32: return operation(Op::BitwiseOr32, value, context, depth, 2); + case IrOpcode::BitwiseXor32: return operation(Op::BitwiseXor32, value, context, depth, 2); + case IrOpcode::BitwiseNot32: return operation(Op::BitwiseNot32, value, context, depth, 1); + case IrOpcode::ShiftLeftLogical32: return operation(Op::ShiftLeftLogical32, value, context, depth, 2); + case IrOpcode::ShiftLeftLogical64: return operation(Op::ShiftLeftLogical64, value, context, depth, 2); + case IrOpcode::ShiftRightLogical32: return operation(Op::ShiftRightLogical32, value, context, depth, 2); + case IrOpcode::ShiftRightLogical64: return operation(Op::ShiftRightLogical64, value, context, depth, 2); + case IrOpcode::ShiftRightArithmetic32: return operation(Op::ShiftRightArithmetic32, value, context, depth, 2); + case IrOpcode::ShiftRightArithmetic64: return operation(Op::ShiftRightArithmetic64, value, context, depth, 2); + case IrOpcode::BitFieldUExtract: return operation(Op::BitFieldUExtract, value, context, depth, 3); + case IrOpcode::BitFieldSExtract: return operation(Op::BitFieldSExtract, value, context, depth, 3); + case IrOpcode::BitFieldInsert: return operation(Op::BitFieldInsert, value, context, depth, 4); + case IrOpcode::SelectU32: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: return operation(Op::Select, value, context, depth, 3); + case IrOpcode::IEqual32: return operation(Op::IEqual32, value, context, depth, 2); + case IrOpcode::INotEqual32: return operation(Op::INotEqual32, value, context, depth, 2); + case IrOpcode::SLessThan32: return operation(Op::SLessThan32, value, context, depth, 2); + case IrOpcode::ULessThan32: return operation(Op::ULessThan32, value, context, depth, 2); + case IrOpcode::UGreaterThan32: return operation(Op::UGreaterThan32, value, context, depth, 2); + case IrOpcode::LogicalAnd: return operation(Op::LogicalAnd, value, context, depth, 2); + case IrOpcode::LogicalOr: return operation(Op::LogicalOr, value, context, depth, 2); + case IrOpcode::LogicalXor: return operation(Op::LogicalXor, value, context, depth, 2); + case IrOpcode::LogicalNot: return operation(Op::LogicalNot, value, context, depth, 1); + default: return fail(); + } + } + + const IrResourcePlan& plan; + SrtTape& tape; + std::vector contexts; + std::map, std::uint32_t> built; + std::uint32_t failNode = SrtTape::NoNode; + bool malformed = false; +}; + +bool SrtTape::Enabled() { + static const bool enabled = std::getenv("APS5_NO_SRT_TAPE") == nullptr && std::getenv("APS5_SRT_DEBUG") == nullptr; + return enabled; +} + +bool SrtTape::Verified() { + static const bool verified = std::getenv("APS5_VERIFY_SRT_TAPE") != nullptr; + return verified; +} + +std::shared_ptr SrtTape::Compile(const IrResourcePlan& plan) { + if (!plan.srtPlanComplete) return nullptr; + auto tape = std::make_shared(); + SrtTapeCompiler compiler(plan, *tape); + if (!compiler.Build()) return nullptr; + return tape; +} + +const SrtTape* SrtTape::For(const IrResourcePlan& plan) { + auto* slot = plan.tapeSlot.get(); + if (slot == nullptr || !Enabled()) return nullptr; + if (!slot->ready.load(std::memory_order_acquire)) { + std::call_once(slot->once, [&] { + try { + slot->tape = Compile(plan); + } catch (...) { + slot->tape = nullptr; + } + }); + slot->ready.store(true, std::memory_order_release); + } + return static_cast(slot->tape.get()); +} + +struct SrtTape::Frame { + const SrtRuntime& runtime; + std::uint64_t* values; + std::uint8_t* states; + bool nullRoot = false; +}; + +bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) const { + const auto state = frame.states[index]; + if (state == 1) { + result = frame.values[index]; + return true; + } + if (state == 2) return false; + std::uint64_t value = 0; + if (!compute(frame, nodes[index], value)) { + frame.states[index] = 2; + return false; + } + frame.states[index] = 1; + frame.values[index] = value; + result = value; + return true; +} + +bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) const { + std::array operand{}; + std::int32_t origin = -1; + for (std::uint8_t index = 0; index < node.argumentCount; ++index) { + if (index == 0 && node.baseWord >= 0 && frame.runtime.userDataUse != nullptr && static_cast(node.baseWord) < frame.runtime.userData.size()) { + origin = node.baseWord; + operand[0] = frame.runtime.userData[static_cast(origin)]; + frame.runtime.userDataUse->asBase |= std::uint64_t {1} << origin; + continue; + } + if (!evaluate(frame, node.arguments[index], operand[index])) return false; + } + const auto a = operand[0]; + const auto b = operand[1]; + const auto c = operand[2]; + const auto d = operand[3]; + switch (node.op) { + case Op::Fail: return false; + case Op::Const: result = node.immediate; return true; + case Op::UserData: + if (node.immediate >= frame.runtime.userData.size()) return false; + if (frame.runtime.userDataUse != nullptr) frame.runtime.userDataUse->asValue |= node.immediate < SrtUserDataUse::TrackedWords ? std::uint64_t {1} << node.immediate : ~std::uint64_t {0}; + result = frame.runtime.userData[static_cast(node.immediate)]; + return true; + case Op::ShaderBase: result = frame.runtime.shaderBase; return true; + case Op::Read: + case Op::ReadConstBuffer: { + const auto base = ((b << 32u) | static_cast(a)) & AddressMask; + const auto immediate = static_cast(node.immediate); + std::uint64_t address = 0; + if (node.op == Op::ReadConstBuffer) { + const auto records = d; + const auto byteOffset = static_cast(immediate) + static_cast(c); + const auto aligned = byteOffset & ~std::uint64_t {3}; + const auto stride = (static_cast(b) >> 16u) & 0x3fffu; + const auto size = stride == 0u ? static_cast(static_cast(records)) : static_cast(stride) * static_cast(records); + if (aligned > size || size - aligned < sizeof(std::uint32_t)) { + result = 0; + return true; + } + address = ((base & ~std::uint64_t {3}) + byteOffset) & ~std::uint64_t {3}; + } else { + const auto relative = (immediate & ~std::int64_t {3}) + static_cast(static_cast(c) & ~3u); + if (!AddSignedAddress(base & ~std::uint64_t {3}, relative, address)) return false; + } + if (base == 0u && !node.loaded) { + frame.nullRoot = true; + return false; + } + if (frame.runtime.userDataUse != nullptr) frame.runtime.userDataUse->reads.emplace_back(address, origin); + if (node.loaded && frame.runtime.accessible != nullptr && !frame.runtime.accessible(frame.runtime.userContext, address, sizeof(std::uint32_t))) return false; + if (auto* trace = frame.runtime.readTrace; trace != nullptr) { + if (node.source == trace->leaf) trace->leaves.emplace_back(trace->leafSlot, address); + else trace->otherReads.push_back(address); + } + const auto reader = node.clean ? frame.runtime.readSpecializationMemory : frame.runtime.readMemory; + std::uint32_t word = 0; + if (reader != nullptr) { + if (!reader(frame.runtime.userContext, address, &word)) return false; + } else { + std::memcpy(&word, reinterpret_cast(address), sizeof(word)); + } + result = word; + return true; + } + case Op::ExtractHalf: result = static_cast(a >> (node.component * 32u)); return true; + case Op::AddCarry: { + const auto sum = static_cast(static_cast(a)) + static_cast(b); + result = node.component == 0u ? static_cast(sum) : static_cast(sum >> 32u); + return true; + } + case Op::ConstructU64: result = static_cast(a) | (static_cast(static_cast(b)) << 32u); return true; + case Op::ConvertF32U32: result = float32Bits(static_cast(static_cast(a))); return true; + case Op::ConvertU32F32: { + const auto value = float32(a); + if (!std::isfinite(value) || value < 0.0f || static_cast(value) > 4294967295.0) return false; + result = static_cast(value); + return true; + } + case Op::FPMul32: result = float32Bits(float32(a) * float32(b)); return true; + case Op::FPTrunc32: result = float32Bits(std::trunc(float32(a))); return true; + case Op::FPIsNan32: result = std::isnan(float32(a)) ? 1u : 0u; return true; + case Op::FPOrdLessThanEqual32: result = float32(a) <= float32(b) ? 1u : 0u; return true; + case Op::FPOrdGreaterThanEqual32: result = float32(a) >= float32(b) ? 1u : 0u; return true; + case Op::IAdd32: result = static_cast(a + b); return true; + case Op::IAdd64: result = a + b; return true; + case Op::ISub32: result = static_cast(a - b); return true; + case Op::ISub64: result = a - b; return true; + case Op::IMul32: result = static_cast(a * b); return true; + case Op::IMul64: result = a * b; return true; + case Op::SMin32: result = static_cast(std::min(std::bit_cast(static_cast(a)), std::bit_cast(static_cast(b)))); return true; + case Op::UMin32: result = std::min(static_cast(a), static_cast(b)); return true; + case Op::BitwiseAnd32: result = static_cast(a & b); return true; + case Op::BitwiseAnd64: result = a & b; return true; + case Op::BitwiseOr32: result = static_cast(a | b); return true; + case Op::BitwiseXor32: result = static_cast(a ^ b); return true; + case Op::BitwiseNot32: result = ~static_cast(a); return true; + case Op::ShiftLeftLogical32: result = static_cast(a) << (b & 31u); return true; + case Op::ShiftLeftLogical64: result = a << (b & 63u); return true; + case Op::ShiftRightLogical32: result = static_cast(a) >> (b & 31u); return true; + case Op::ShiftRightLogical64: result = a >> (b & 63u); return true; + case Op::ShiftRightArithmetic32: result = static_cast(std::bit_cast(static_cast(a)) >> (b & 31u)); return true; + case Op::ShiftRightArithmetic64: result = static_cast(std::bit_cast(a) >> (b & 63u)); return true; + case Op::BitFieldUExtract: { + const auto offset = static_cast(b); + const auto width = static_cast(c); + if (offset > 32u || width > 32u - offset) return false; + const auto mask = width == 32u ? 0xffffffffu : width == 0u ? 0u : (std::uint32_t {1} << width) - 1u; + result = width == 0u ? 0u : (static_cast(a) >> offset) & mask; + return true; + } + case Op::BitFieldSExtract: { + const auto offset = static_cast(b); + const auto width = static_cast(c); + if (offset > 32u || width > 32u - offset) return false; + if (width == 0u) { + result = 0; + return true; + } + const auto mask = width == 32u ? 0xffffffffu : (std::uint32_t {1} << width) - 1u; + auto bits = (static_cast(a) >> offset) & mask; + if (width < 32u && (bits & (std::uint32_t {1} << (width - 1u))) != 0u) bits |= ~mask; + result = bits; + return true; + } + case Op::BitFieldInsert: { + const auto offset = static_cast(c); + const auto width = static_cast(d); + if (offset > 32u || width > 32u - offset) return false; + if (width == 0u) { + result = static_cast(a); + return true; + } + const auto mask = width == 32u ? 0xffffffffu : ((std::uint32_t {1} << width) - 1u) << offset; + result = (static_cast(a) & ~mask) | ((static_cast(b) << offset) & mask); + return true; + } + case Op::Select: result = a != 0u ? b : c; return true; + case Op::IEqual32: result = static_cast(a) == static_cast(b) ? 1u : 0u; return true; + case Op::INotEqual32: result = static_cast(a) != static_cast(b) ? 1u : 0u; return true; + case Op::SLessThan32: result = std::bit_cast(static_cast(a)) < std::bit_cast(static_cast(b)) ? 1u : 0u; return true; + case Op::ULessThan32: result = static_cast(a) < static_cast(b) ? 1u : 0u; return true; + case Op::UGreaterThan32: result = static_cast(a) > static_cast(b) ? 1u : 0u; return true; + case Op::LogicalAnd: result = (a != 0u) && (b != 0u) ? 1u : 0u; return true; + case Op::LogicalOr: result = (a != 0u) || (b != 0u) ? 1u : 0u; return true; + case Op::LogicalXor: result = (a != 0u) != (b != 0u) ? 1u : 0u; return true; + case Op::LogicalNot: result = a == 0u ? 1u : 0u; return true; + } + return false; +} + +bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::vector& activeSources) const { + if (!plan.srtPlanComplete) return false; + if (std::any_of(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end(), [](std::uint8_t clean) { return clean != 0u; }) && runtime.readSpecializationMemory == nullptr) return false; + auto* trace = runtime.readTrace; + const auto leavesBefore = trace != nullptr ? trace->leaves.size() : 0; + const auto readsBefore = trace != nullptr ? trace->otherReads.size() : 0; + auto* use = runtime.userDataUse; + const auto baseBefore = use != nullptr ? use->asBase : 0; + const auto valueBefore = use != nullptr ? use->asValue : 0; + const auto useReadsBefore = use != nullptr ? use->reads.size() : 0; + const auto refuse = [&] { + if (trace != nullptr) { + trace->leaves.resize(leavesBefore); + trace->otherReads.resize(readsBefore); + trace->leaf = nullptr; + } + if (use != nullptr) { + use->asBase = baseBefore; + use->asValue = valueBefore; + use->reads.resize(useReadsBefore); + } + return false; + }; + constexpr std::size_t InlineNodes = 512; + std::array inlineValues; + std::array inlineStates; + std::vector heapValues; + std::vector heapStates; + std::uint64_t* values = inlineValues.data(); + std::uint8_t* states = inlineStates.data(); + if (nodes.size() > InlineNodes) { + heapValues.resize(nodes.size()); + heapStates.assign(nodes.size(), 0u); + values = heapValues.data(); + states = heapStates.data(); + } else { + std::fill_n(states, nodes.size(), std::uint8_t {0}); + } + Frame frame {runtime, values, states}; + + struct Scratch { + std::vector active; + std::vector visited; + std::vector pending; + std::vector evaluated; + std::vector flattened; + }; + struct ScratchStorage {}; + auto& scratch = HostThreadLocal(); + auto& active = scratch.active; + const auto imageBase = plan.descriptorSources.size(); + active.assign(imageBase + plan.info.images.size(), 1u); + if (!plan.controlFlow.empty()) { + for (const auto& block : plan.controlFlow) { + for (const auto source : block.sources) { + if (source >= active.size()) return refuse(); + active[source] = 0u; + } + for (const auto image : block.images) { + if (image >= plan.info.images.size()) return refuse(); + active[imageBase + image] = 0u; + } + } + auto& visited = scratch.visited; + visited.assign(plan.controlFlow.size(), 0u); + auto& pending = scratch.pending; + pending.assign(1, 0u); + while (!pending.empty()) { + const auto index = pending.back(); + pending.pop_back(); + if (index >= visited.size()) return refuse(); + if (visited[index]) continue; + visited[index] = 1u; + const auto& block = plan.controlFlow[index]; + for (const auto source : block.sources) active[source] = 1u; + for (const auto image : block.images) active[imageBase + image] = 1u; + std::uint64_t condition = 0; + const bool cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition); + if (frame.nullRoot) return refuse(); + if (cleanEvaluable) { + if (block.successors.size() < 2) return refuse(); + pending.push_back(block.successors[static_cast(condition) != 0u ? 0u : 1u]); + } else { + pending.insert(pending.end(), block.successors.begin(), block.successors.end()); + } + } + } + auto& evaluated = scratch.evaluated; + evaluated.clear(); + evaluated.reserve(plan.materializationSources.size()); + for (const auto sourceIndex : plan.materializationSources) { + if (sourceIndex >= plan.descriptorSources.size()) return refuse(); + const auto& source = plan.descriptorSources[sourceIndex]; + DescriptorValue value; + value.dwordCount = source.dwordCount; + if (active[sourceIndex]) { + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) { + std::uint64_t word = 0; + if (!evaluate(frame, sourceDwords[sourceIndex][dword], word)) return refuse(); + value.dwords[dword] = static_cast(word); + } + } + evaluated.push_back(value); + } + auto& flattened = scratch.flattened; + flattened.assign(plan.srtReads.size(), 0u); + for (std::size_t slot = 0; slot < plan.srtReads.size(); ++slot) { + const auto& read = plan.srtReads[slot]; + const bool pure = trace != nullptr && read.flatOffset < plan.pureFlatSlots.size() && plan.pureFlatSlots[read.flatOffset] != 0u; + if (pure) { + trace->leaf = read.value->Resolve(); + trace->leafSlot = read.flatOffset; + } + std::uint64_t word = 0; + const bool ok = read.flatOffset < flattened.size() && evaluate(frame, flatRoots[slot], word); + if (pure) trace->leaf = nullptr; + if (!ok) return refuse(); + flattened[read.flatOffset] = static_cast(word); + } + results.swap(evaluated); + activeSources.swap(active); + flat.assign(flattened.begin(), flattened.end()); + return true; +} + +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 32dcb23ba2..d08bc8a148 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,20 @@ 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; + targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; 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 +564,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 +881,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 +1009,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 +1344,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..38de551273 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 @@ -111,7 +112,8 @@ enum class ConservativeZExport : std::uint8_t { enum class ColorExportPacking : std::uint8_t { None, - Unorm10_11_11 + Unorm10_11_11, + Float11_11_10 }; enum class RasterPrimitive : std::uint8_t { @@ -266,6 +268,8 @@ struct SpirvTarget { std::uint32_t registrationWordLimit = 200000u; bool vertexSubgroups = true; bool float16DenormPreserve = false; + bool spannedWaves = false; + bool robustBufferAccess2 = false; }; struct BindingLayout { @@ -302,6 +306,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 +396,8 @@ enum class DescriptorRole { ShaderData, ImageTable, SamplerTable, - ImageTableMap + ImageTableMap, + ImageStorageTable }; struct DescriptorBinding { @@ -468,25 +474,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 +548,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 +607,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 +631,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 +686,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); @@ -612,6 +701,13 @@ struct GeometryStageLimits { [[nodiscard]] RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional& limits); +struct BarycentricTessellationShaders { + RecompileResult control; + RecompileResult evaluation; +}; + +[[nodiscard]] BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents); + } #endif diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 99358baf0d..52d25ba439 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 }; @@ -40,9 +41,9 @@ inline constexpr std::uint32_t BufferCapacity = 128u; inline constexpr std::uint32_t ImageCapacity = 256u; 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 StorageMipSlots = 16u; +inline constexpr std::uint32_t StorageHeapCapacity = 64u; +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 0656a8fb3f..3c9e663e94 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,15 +101,15 @@ 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"); +static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 168, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 88, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -145,11 +145,11 @@ static_assert(fieldCount() == 8, "SamplerResource changed: upda static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); -static_assert(fieldCount() == 23, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(fieldCount() == 24, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 15, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 17, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); @@ -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); @@ -1045,6 +1047,8 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { out.Value(column.indirectImmediate); out.Value(column.foldedAddend); out.Value(column.foldedCarry); + out.Value(column.selected); + out.Value(column.selection); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1200,6 +1204,8 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { in.Value(column.indirectImmediate); in.Value(column.foldedAddend); in.Value(column.foldedCarry); + in.Value(column.selected); + in.Value(column.selection); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { @@ -1232,7 +1238,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; - if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false; + if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && (!source.tableColumn->selected || knownSource(source.tableColumn->selection)) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false; if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false; @@ -1311,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(); } @@ -1363,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); @@ -1520,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(); @@ -1531,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(); @@ -1543,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) { @@ -1628,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}; @@ -1637,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; } @@ -1694,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; } @@ -1795,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 b03d85a809..f9840a0963 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 36; +inline constexpr std::uint32_t FormatVersion = 37; enum class LoadStatus { Loaded, @@ -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/SpirvClipDistances.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvClipDistances.hpp new file mode 100644 index 0000000000..2d915b38eb --- /dev/null +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvClipDistances.hpp @@ -0,0 +1,46 @@ +#ifndef CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVCLIPDISTANCES_HPP +#define CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVCLIPDISTANCES_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +[[nodiscard]] inline std::uint32_t VertexClipDistanceCount(std::span words) { + std::set builtins; + std::map pointees; + std::map arrays; + std::map constants; + std::map outputs; + for (std::size_t i = 5; i < words.size();) { + const auto count = words[i] >> 16u; + const auto opcode = words[i] & 0xffffu; + if (count == 0u || i + count > words.size()) throw std::runtime_error("Clip distances: malformed vertex SPIR-V"); + const auto* word = words.data() + i; + if (opcode == spv::OpDecorate && count == 4u && word[2] == spv::DecorationBuiltIn && word[3] == spv::BuiltInClipDistance) builtins.insert(word[1]); + if (opcode == spv::OpTypePointer && count == 4u) pointees[word[1]] = word[3]; + if (opcode == spv::OpTypeArray && count == 4u) arrays[word[1]] = word[3]; + if (opcode == spv::OpConstant && count == 4u) constants[word[2]] = word[3]; + if (opcode == spv::OpVariable && count >= 4u && word[3] == spv::StorageClassOutput) outputs[word[2]] = word[1]; + i += count; + } + for (const auto variable : builtins) { + const auto output = outputs.find(variable); + if (output == outputs.end()) continue; + const auto pointee = pointees.find(output->second); + const auto array = pointee == pointees.end() ? arrays.end() : arrays.find(pointee->second); + const auto length = array == arrays.end() ? constants.end() : constants.find(array->second); + if (length == constants.end() || length->second == 0u || length->second > 8u) throw std::runtime_error("Clip distances: the vertex gl_ClipDistance output is not an array of one to eight elements"); + return length->second; + } + return 0u; +} + +} + +#endif diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 9d22da5885..7f7ccbdec5 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,22 @@ struct SpirvTargetOptions { bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool spannedWaves = false; + bool robustBufferAccess2 = 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/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index dcd4b95b44..836b58a04c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -117,6 +117,10 @@ std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResou std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); +std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value); +std::uint32_t LdsStorageDwords(const SpirvEmitterState& state); +bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access); +std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value); bool IsSignedFormatComponent(SpirvFormatComponentType type); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 2ac59b0477..e293dd87c7 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -83,6 +83,7 @@ struct SpirvTableAccess { std::uint32_t dword3 = 0; std::uint32_t samplerElement = 0; bool active = false; + bool uniform = false; }; struct SpirvEmitterState { @@ -100,11 +101,14 @@ 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; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool robustBufferAccess2 = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; std::uint32_t spirvVersion = 0x00010300u; @@ -185,6 +189,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; @@ -211,6 +216,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/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index 7c86bc826b..daca4351b0 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -1,4 +1,5 @@ #include "Recompiler.hpp" +#include "SpirvBackend/SpirvClipDistances.hpp" #include "SpirvBackend/SpirvModule.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -24,6 +25,38 @@ struct PerVertexParameter { std::uint32_t location; }; +struct EmulatedInterface { + std::vector passed; + std::vector passedFlat; + std::vector perVertex; + BarycentricEmulationLayout layout; + std::uint32_t barycentrics = 0; + std::uint32_t sources = 0; +}; + +EmulatedInterface CollectEmulatedInterface(const RecompileResult& vertex, const RecompileResult& fragment) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + const auto exported = [&](std::uint32_t location) { return std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), location) != vertex.parameterExports.end(); }; + EmulatedInterface emulated; + emulated.layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); + for (const auto& parameter : fragment.fragmentParameters) { + require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); + if (!parameter.perVertex) { + emulated.passed.push_back(parameter.sourceLocation); + emulated.passedFlat.push_back(parameter.flat); + continue; + } + const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); + require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); + emulated.perVertex.push_back({parameter.sourceLocation, relocated->second}); + } + emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); + std::set sources(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation); + emulated.sources = static_cast(sources.size()); + return emulated; +} + class BarycentricGeometryEmitter { public: explicit BarycentricGeometryEmitter(std::uint32_t version) : builder(version) { @@ -140,6 +173,217 @@ private: } }; +class BarycentricTessellationEmitter { +public: + BarycentricTessellationEmitter(std::uint32_t version, std::uint32_t clipDistances) : builder(version), clipDistances(clipDistances) { + builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + voidType = builder.Type(spv::OpTypeVoid); + boolType = builder.Type(spv::OpTypeBool); + uintType = builder.Type(spv::OpTypeInt, 32u, 0u); + intType = builder.Type(spv::OpTypeInt, 32u, 1u); + floatType = builder.Type(spv::OpTypeFloat, 32u); + vec3FloatType = builder.Type(spv::OpTypeVector, floatType, 3u); + vec4FloatType = builder.Type(spv::OpTypeVector, floatType, 4u); + vertexArrayType = builder.Type(spv::OpTypeArray, vec4FloatType, constant(uintType, 3u)); + functionType = builder.Type(spv::OpTypeFunction, voidType); + if (clipDistances != 0u) { + clipArrayType = builder.Type(spv::OpTypeArray, floatType, constant(uintType, clipDistances)); + controlInputClipType = builder.Type(spv::OpTypeArray, clipArrayType, constant(uintType, 3u)); + ptrControlInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, clipArrayType); + ptrControlOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); + ptrEvaluationInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, clipArrayType); + ptrEvaluationOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); + } + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + perVertexArrayType = builder.Type(spv::OpTypeArray, perVertexType, constant(uintType, 3u)); + ptrInputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassInput, vec4FloatType); + ptrOutputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, vec4FloatType); + } + + std::vector EmitControl(const std::vector& sources) { + begin(); + builder.AddExecutionMode(main, spv::ExecutionModeOutputVertices, 3u); + addDomain(); + const auto invocationId = addInterface(spv::StorageClassInput, intType); + decorate(invocationId, spv::DecorationBuiltIn, spv::BuiltInInvocationId); + const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); + const auto glOut = addInterface(spv::StorageClassOutput, perVertexArrayType); + std::uint32_t clipIn = 0u; + std::uint32_t clipOut = 0u; + if (clipDistances != 0u) { + clipIn = addInterface(spv::StorageClassInput, controlInputClipType); + decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + clipOut = addInterface(spv::StorageClassOutput, controlInputClipType); + decorate(clipOut, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + } + const auto floatArray = [&](std::uint32_t size) { return builder.Type(spv::OpTypeArray, floatType, constant(uintType, size)); }; + const auto outer = addInterface(spv::StorageClassOutput, floatArray(4u)); + decorate(outer, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); + builder.AddAnnotation(spv::OpDecorate, outer, spv::DecorationPatch); + const auto inner = addInterface(spv::StorageClassOutput, floatArray(2u)); + decorate(inner, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); + builder.AddAnnotation(spv::OpDecorate, inner, spv::DecorationPatch); + std::vector> parameters; + for (const auto location : sources) { + const auto input = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(input, spv::DecorationLocation, location); + const auto output = addInterface(spv::StorageClassOutput, vertexArrayType); + decorate(output, spv::DecorationLocation, location); + parameters.emplace_back(input, output); + } + builder.EmitEntryPoint(spv::ExecutionModelTessellationControl, main, "main", interfaces); + builder.AddFunction(spv::OpLabel, builder.AllocateId()); + + const auto invocation = load(intType, invocationId); + const auto first = result(spv::OpIEqual, boolType, invocation, constant(intType, 0u)); + const auto writeLevels = builder.AllocateId(); + const auto afterLevels = builder.AllocateId(); + builder.AddFunction(spv::OpSelectionMerge, afterLevels, spv::SelectionControlMaskNone); + builder.AddFunction(spv::OpBranchConditional, first, writeLevels, afterLevels); + builder.AddFunction(spv::OpLabel, writeLevels); + const auto ptrOutputFloat = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, floatType); + const auto one = constant(floatType, std::bit_cast(1.0f)); + for (std::uint32_t i = 0; i < 4u; ++i) store(result(spv::OpAccessChain, ptrOutputFloat, outer, constant(intType, i)), one); + for (std::uint32_t i = 0; i < 2u; ++i) store(result(spv::OpAccessChain, ptrOutputFloat, inner, constant(intType, i)), one); + builder.AddFunction(spv::OpBranch, afterLevels); + builder.AddFunction(spv::OpLabel, afterLevels); + store(result(spv::OpAccessChain, ptrOutputVec4, glOut, invocation, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, invocation, constant(intType, 0u)))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrControlOutputClip, clipOut, invocation), load(clipArrayType, result(spv::OpAccessChain, ptrControlInputClip, clipIn, invocation))); + for (const auto& [input, output] : parameters) store(result(spv::OpAccessChain, ptrOutputVec4, output, invocation), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, input, invocation))); + return finish(); + } + + std::vector EmitEvaluation(const std::vector& sources, const std::vector& passed, const std::vector& passedFlat, const std::vector& perVertex, const BarycentricEmulationLayout& layout) { + begin(); + addDomain(); + const auto tessCoord = addInterface(spv::StorageClassInput, vec3FloatType); + decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); + const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); + std::uint32_t clipIn = 0u; + if (clipDistances != 0u) { + clipIn = addInterface(spv::StorageClassInput, controlInputClipType); + decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + } + std::array inputs {}; + for (const auto location : sources) { + inputs[location] = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(inputs[location], spv::DecorationLocation, location); + } + const auto glOut = addInterface(spv::StorageClassOutput, perVertexType); + if (clipDistances != 0u) { + evaluationClipOutput = addInterface(spv::StorageClassOutput, clipArrayType); + decorate(evaluationClipOutput, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + } + std::vector passedOutputs; + for (const auto location : passed) { + passedOutputs.push_back(addInterface(spv::StorageClassOutput, vec4FloatType)); + decorate(passedOutputs.back(), spv::DecorationLocation, location); + } + const auto barycentricOutput = [&](std::uint32_t location) { + if (location == BarycentricEmulationLayout::NoLocation) return 0u; + const auto variable = addInterface(spv::StorageClassOutput, vec3FloatType); + decorate(variable, spv::DecorationLocation, location); + return variable; + }; + const auto smooth = barycentricOutput(layout.smoothLocation); + const auto linear = barycentricOutput(layout.linearLocation); + std::vector perVertexOutputs; + for (const auto& parameter : perVertex) { + perVertexOutputs.push_back(addInterface(spv::StorageClassOutput, vertexArrayType)); + decorate(perVertexOutputs.back(), spv::DecorationLocation, parameter.location); + } + builder.EmitEntryPoint(spv::ExecutionModelTessellationEvaluation, main, "main", interfaces); + builder.AddFunction(spv::OpLabel, builder.AllocateId()); + + const auto ptrInputFloat = builder.Type(spv::OpTypePointer, spv::StorageClassInput, floatType); + const auto half = constant(floatType, std::bit_cast(0.5f)); + const auto zero = constant(floatType, 0u); + const auto one = constant(floatType, std::bit_cast(1.0f)); + std::array corner {}; + for (std::uint32_t i = 0; i < 3u; ++i) corner[i] = result(spv::OpFOrdGreaterThan, boolType, load(floatType, result(spv::OpAccessChain, ptrInputFloat, tessCoord, constant(intType, i))), half); + auto index = result(spv::OpSelect, intType, corner[2], constant(intType, 2u), constant(intType, 0u)); + index = result(spv::OpSelect, intType, corner[1], constant(intType, 1u), index); + const auto weight = [&](std::uint32_t vertex) { return result(spv::OpSelect, floatType, result(spv::OpIEqual, boolType, index, constant(intType, vertex)), one, zero); }; + const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, weight(0u), weight(1u), weight(2u)); + + store(result(spv::OpAccessChain, ptrOutputVec4, glOut, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, index, constant(intType, 0u)))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrEvaluationOutputClip, evaluationClipOutput), load(clipArrayType, result(spv::OpAccessChain, ptrEvaluationInputClip, clipIn, index))); + for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, 0u) : index))); + if (smooth != 0u) store(smooth, weights); + if (linear != 0u) store(linear, weights); + for (std::size_t i = 0; i < perVertex.size(); ++i) { + for (std::uint32_t source = 0; source < 3u; ++source) store(result(spv::OpAccessChain, ptrOutputVec4, perVertexOutputs[i], constant(intType, source)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[perVertex[i].sourceLocation], constant(intType, source)))); + } + return finish(); + } + +private: + SpirvModule builder; + std::uint32_t clipDistances; + std::vector interfaces; + std::uint32_t main = 0; + std::uint32_t voidType = 0; + std::uint32_t boolType = 0; + std::uint32_t uintType = 0; + std::uint32_t intType = 0; + std::uint32_t floatType = 0; + std::uint32_t vec3FloatType = 0; + std::uint32_t vec4FloatType = 0; + std::uint32_t vertexArrayType = 0; + std::uint32_t functionType = 0; + std::uint32_t perVertexType = 0; + std::uint32_t perVertexArrayType = 0; + std::uint32_t clipArrayType = 0; + std::uint32_t controlInputClipType = 0; + std::uint32_t ptrInputVec4 = 0; + std::uint32_t ptrOutputVec4 = 0; + std::uint32_t ptrControlInputClip = 0; + std::uint32_t ptrControlOutputClip = 0; + std::uint32_t ptrEvaluationInputClip = 0; + std::uint32_t ptrEvaluationOutputClip = 0; + std::uint32_t evaluationClipOutput = 0; + + void begin() { + builder.EmitCapability(spv::CapabilityShader); + builder.EmitCapability(spv::CapabilityTessellation); + if (clipDistances != 0u) builder.EmitCapability(spv::CapabilityClipDistance); + main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); + } + + void addDomain() { + builder.AddExecutionMode(main, spv::ExecutionModeTriangles); + builder.AddExecutionMode(main, spv::ExecutionModeSpacingEqual); + builder.AddExecutionMode(main, spv::ExecutionModeVertexOrderCw); + } + + std::vector finish() { + builder.AddFunction(spv::OpReturn); + builder.AddFunction(spv::OpFunctionEnd); + return builder.Finalize(); + } + + std::uint32_t constant(std::uint32_t type, std::uint32_t value) { return builder.Constant(spv::OpConstant, type, value); } + + template + std::uint32_t result(spv::Op opcode, std::uint32_t type, TArgs... operands) { + const auto id = builder.AllocateId(); + builder.AddFunction(opcode, type, id, operands...); + return id; + } + + std::uint32_t load(std::uint32_t type, std::uint32_t pointer) { return result(spv::OpLoad, type, pointer); } + + void store(std::uint32_t pointer, std::uint32_t value) { builder.AddFunction(spv::OpStore, pointer, value); } + + void decorate(std::uint32_t target, spv::Decoration decoration, std::uint32_t value) { builder.AddAnnotation(spv::OpDecorate, target, decoration, value); } + + std::uint32_t addInterface(spv::StorageClass storage, std::uint32_t type) { + const auto variable = builder.DefineGlobalVariable(builder.Type(spv::OpTypePointer, storage, type), storage); + interfaces.push_back(variable); + return variable; + } +}; + } BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation) { @@ -171,34 +415,48 @@ RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, co require(limits.has_value(), "geometry shaders are unavailable"); require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityGeometry) != target.supportedCapabilities.end(), "geometry capability is unavailable"); - const auto exported = [&](std::uint32_t location) { return std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), location) != vertex.parameterExports.end(); }; - const auto layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); - std::vector passed; - std::vector perVertex; - for (const auto& parameter : fragment.fragmentParameters) { - require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); - if (!parameter.perVertex) { - passed.push_back(parameter.sourceLocation); - continue; - } - const auto relocated = std::find_if(layout.perVertexLocations.begin(), layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); - require(relocated != layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); - perVertex.push_back({parameter.sourceLocation, relocated->second}); - } - const auto barycentrics = (layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); - const auto outputComponents = 4u + 4u * static_cast(passed.size()) + 3u * barycentrics + 12u * static_cast(perVertex.size()); - std::set sources(passed.begin(), passed.end()); - for (const auto& parameter : perVertex) sources.insert(parameter.sourceLocation); - const auto inputComponents = 4u + 4u * static_cast(sources.size()); - const auto fragmentComponents = 4u * (static_cast(passed.size()) + barycentrics + 3u * static_cast(perVertex.size())); + const auto emulated = CollectEmulatedInterface(vertex, fragment); + const auto passed = static_cast(emulated.passed.size()); + const auto perVertex = static_cast(emulated.perVertex.size()); + const auto outputComponents = 4u + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; + const auto inputComponents = 4u + 4u * emulated.sources; + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); require(limits->maxGeometryOutputVertices >= 3u && outputComponents <= limits->maxGeometryOutputComponents && 3u * outputComponents <= limits->maxGeometryTotalOutputComponents && inputComponents <= limits->maxGeometryInputComponents && fragmentComponents <= limits->maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); BarycentricGeometryEmitter emitter(target.spirvVersion); RecompileResult result; - result.spirv = emitter.Emit(passed, perVertex, layout); + result.spirv = emitter.Emit(emulated.passed, emulated.perVertex, emulated.layout); #if ANYPS5_ENABLE_SPIRV_TOOLS result.spirv = ValidateAndOptimizeSpirv(result.spirv, target.vulkanVersion, target.spirvVersion); #endif return result; } +BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + require(target.tessellation.has_value(), "tessellation shaders are unavailable"); + require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); + require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); + const auto emulated = CollectEmulatedInterface(vertex, fragment); + const auto passed = static_cast(emulated.passed.size()); + const auto perVertex = static_cast(emulated.perVertex.size()); + const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); + require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); + const auto controlComponents = 4u + clipDistances + 4u * emulated.sources; + const auto evaluationOutputComponents = 4u + clipDistances + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); + const auto& limits = *target.tessellation; + require(limits.maxPatchSize >= 3u && controlComponents <= limits.maxControlPerVertexInputComponents && controlComponents <= limits.maxControlPerVertexOutputComponents && 3u * controlComponents + 4u <= limits.maxControlTotalOutputComponents && limits.maxControlPerPatchOutputComponents >= 4u && controlComponents <= limits.maxEvaluationInputComponents && evaluationOutputComponents <= limits.maxEvaluationOutputComponents && fragmentComponents <= maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); + std::set sourceSet(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sourceSet.insert(parameter.sourceLocation); + const std::vector sources(sourceSet.begin(), sourceSet.end()); + BarycentricTessellationShaders shaders; + shaders.control.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitControl(sources); + shaders.evaluation.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitEvaluation(sources, emulated.passed, emulated.passedFlat, emulated.perVertex, emulated.layout); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); + shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); +#endif + return shaders; +} + } diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index 59757fa18c..053a298982 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -1,6 +1,7 @@ #include "Recompiler.hpp" #include "BdaAbi.hpp" #include "RuntimeAbi.hpp" +#include "SpirvBackend/SpirvClipDistances.hpp" #include "SpirvBackend/SpirvModule.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -31,7 +32,7 @@ void require(bool condition, const char* message) { class RectListEmitter { public: - RectListEmitter(const std::vector& parameters, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding) : builder(version), faultBinding(faultBinding), version(version), parameters(parameters) { + RectListEmitter(const std::vector& parameters, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding, std::uint32_t clipDistances) : builder(version), faultBinding(faultBinding), version(version), clipDistances(clipDistances), parameters(parameters) { builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); voidType = type(spv::OpTypeVoid); @@ -41,7 +42,12 @@ public: vec4FloatType = type(spv::OpTypeVector, floatType, 4u); functionType = type(spv::OpTypeFunction, voidType); - perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + if (clipDistances != 0u) { + clipArrayType = array(floatType, clipDistances); + ptrInputClip = pointer(spv::StorageClassInput, clipArrayType); + ptrOutputClip = pointer(spv::StorageClassOutput, clipArrayType); + } ptrInputVec4Float = pointer(spv::StorageClassInput, vec4FloatType); ptrOutputVec4Float = pointer(spv::StorageClassOutput, vec4FloatType); @@ -56,7 +62,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)); @@ -116,6 +122,20 @@ public: const auto position = result(spv::OpSelect, vec4FloatType, isFourth, position3, load(vec4FloatType, access(ptrInputVec4Float, glIn, index, intConstant(0)))); store(access(ptrOutputVec4Float, glOut, invocation, intConstant(0)), position); + if (clipDistances != 0u) { + std::array clips {}; + for (std::uint32_t i = 0; i < clips.size(); i++) clips[i] = load(clipArrayType, access(ptrInputClip, clipIn, intConstant(i))); + const auto own = load(clipArrayType, access(ptrInputClip, clipIn, index)); + std::vector values; + for (std::uint32_t k = 0; k < clipDistances; k++) { + auto sum = constant(floatType, 0); + for (std::uint32_t i = 0; i < clips.size(); i++) sum = result(spv::OpFAdd, floatType, sum, result(spv::OpFMul, floatType, result(spv::OpCompositeExtract, floatType, clips[i], k), barycentric[i])); + values.push_back(result(spv::OpSelect, floatType, fourth, sum, result(spv::OpCompositeExtract, floatType, own, k))); + } + const auto clip = builder.AllocateId(); + builder.AddFunction(spv::OpCompositeConstruct, clipArrayType, clip, std::span(values)); + store(access(ptrOutputClip, clipOut, invocation), clip); + } for (std::uint32_t i = 0; i < parameters.size(); i++) { if (parameters[i].missing) { @@ -123,11 +143,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); @@ -149,6 +169,7 @@ public: const auto position = load(vec4FloatType, access(ptrInputVec4Float, glIn, index, intConstant(0))); store(access(ptrOutputVec4Float, glOut, intConstant(0)), position); + if (clipDistances != 0u) store(clipOut, load(clipArrayType, access(ptrInputClip, clipIn, index))); for (std::uint32_t i = 0; i < parameters.size(); i++) { store(outputs[i], load(vec4FloatType, access(ptrInputVec4Float, inputs[i], index))); } @@ -264,6 +285,12 @@ private: return variable; } + std::uint32_t clipVariable(spv::StorageClass storage, std::uint32_t type) { + const auto variable = addInterface(storage, type); + decorate(variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + return variable; + } + void decorate(std::uint32_t target, spv::Decoration decoration, std::uint32_t value) { builder.AddAnnotation(spv::OpDecorate, target, decoration, value); } @@ -271,6 +298,7 @@ private: void defineEntry(spv::ExecutionModel model) { builder.EmitCapability(spv::CapabilityShader); builder.EmitCapability(spv::CapabilityTessellation); + if (clipDistances != 0u) builder.EmitCapability(spv::CapabilityClipDistance); main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); if (model == spv::ExecutionModelTessellationControl) { builder.AddExecutionMode(main, spv::ExecutionModeOutputVertices, 4u); @@ -296,6 +324,7 @@ private: decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); } glIn = addInterface(spv::StorageClassInput, array(perVertexType, tessControl ? 3u : 4u)); + if (clipDistances != 0u) clipIn = clipVariable(spv::StorageClassInput, array(clipArrayType, tessControl ? 3u : 4u)); inputs.resize(parameters.size()); std::array locations {}; @@ -316,6 +345,7 @@ private: const auto tessControl = model == spv::ExecutionModelTessellationControl; if (tessControl) { glOut = addInterface(spv::StorageClassOutput, array(perVertexType, 4u)); + if (clipDistances != 0u) clipOut = clipVariable(spv::StorageClassOutput, array(clipArrayType, 4u)); tessInner = addInterface(spv::StorageClassOutput, array(floatType, 2u)); decorate(tessInner, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); builder.AddAnnotation(spv::OpDecorate, tessInner, spv::DecorationPatch); @@ -324,6 +354,7 @@ private: builder.AddAnnotation(spv::OpDecorate, tessOuter, spv::DecorationPatch); } else { glOut = addInterface(spv::StorageClassOutput, perVertexType); + if (clipDistances != 0u) clipOut = clipVariable(spv::StorageClassOutput, clipArrayType); } outputs.resize(parameters.size()); @@ -343,6 +374,7 @@ private: SpirvModule builder; std::uint32_t faultBinding; std::uint32_t version; + std::uint32_t clipDistances; std::uint32_t fault = 0; const std::vector& parameters; std::vector interfaces; @@ -360,12 +392,17 @@ private: std::uint32_t vec4FloatType = 0; std::uint32_t functionType = 0; std::uint32_t perVertexType = 0; + std::uint32_t clipArrayType = 0; + std::uint32_t ptrInputClip = 0; + std::uint32_t ptrOutputClip = 0; std::uint32_t ptrInputFloat = 0; std::uint32_t ptrInputVec4Float = 0; std::uint32_t ptrOutputFloat = 0; std::uint32_t ptrOutputVec4Float = 0; std::uint32_t glIn = 0; std::uint32_t glOut = 0; + std::uint32_t clipIn = 0; + std::uint32_t clipOut = 0; std::uint32_t tessInner = 0; std::uint32_t tessOuter = 0; std::uint32_t tessCoord = 0; @@ -394,11 +431,13 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp } const auto components = static_cast((parameters.size() + 1) * 4); require(limits.maxPatchSize >= 4 && components <= limits.maxControlPerVertexInputComponents && components <= limits.maxControlPerVertexOutputComponents && components <= limits.maxEvaluationInputComponents && components <= limits.maxEvaluationOutputComponents && limits.maxControlPerPatchOutputComponents >= 6 && components * 4 + 6 <= limits.maxControlTotalOutputComponents, "tessellation interface exceeds device limits"); + const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); + require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); const auto faultBinding = RuntimeAbi::BindingNumber(RuntimeAbi::Stage::TessellationControl, RuntimeAbi::Binding::FaultBuffer); - RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); - RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding); + RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding, clipDistances); 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 +445,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, clipDistances); + 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..cbf8912a45 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)); } @@ -296,7 +295,9 @@ bool AllOnesWord(const IrValue* word) { } bool AllLanesBit(const IrValue* bit) { - const auto words = LaneBitWords(Resolved(bit)); + bit = Resolved(bit); + if (Is(bit, IrOpcode::LogicalOr) && EitherOrder(bit, [](const IrValue* inverted, const IrValue* other) { return Is(inverted, IrOpcode::LogicalNot) && Resolved(inverted->Argument(0)) == other; })) return true; + const auto words = LaneBitWords(bit); return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } @@ -389,7 +390,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 +567,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; @@ -597,6 +598,69 @@ std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c return result; } +bool ActiveMaskedWord(const IrValue* word, std::uint32_t depth = 0u) { + word = Resolved(word); + if (word == nullptr || depth > 8u) return false; + if (Is(word, IrOpcode::CompositeExtractU32x4)) { + const auto* ballot = Resolved(word->Argument(0)); + const auto* active = ballot != nullptr && Is(ballot, IrOpcode::Ballot) ? Resolved(ballot->Argument(0)) : nullptr; + return active != nullptr && active->HasImmediate() && active->Type() == IrType::Bool && active->ImmediateBool() && IsU32(word->Argument(1), 0u); + } + return Is(word, IrOpcode::BitwiseAnd32) && (ActiveMaskedWord(word->Argument(0), depth + 1u) || ActiveMaskedWord(word->Argument(1), depth + 1u)); +} + +bool ActiveMaskedBit(const IrValue* bit) { + const auto words = LaneBitWords(Resolved(bit)); + return words && ActiveMaskedWord((*words)[0]) && ActiveMaskedWord((*words)[1]); +} + +std::optional MatchRowScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* top = Unmasked(Resolved(value), narrow); + for (const auto& reduction : WaveReductions) { + if (reduction.floating) continue; + const IrValue* scan = top; + for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { + scan = RowScanStepInput(scan, reduction, control, narrow); + if (scan == nullptr) break; + } + if (scan == nullptr) continue; + const auto* source = Unmasked(scan, narrow); + if (Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction.identity) && (ActiveMaskedBit(source->Argument(0)) || EntryLaneWalk{stage}.ZeroBit(source->Argument(0)))) return HalfWaveScan{&reduction, source}; + } + return std::nullopt; +} + +std::optional EmitRowReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { + auto& state = ctx.state; + const bool split = state.splitSubgroup && state.hostSubgroupSize == 64u && state.program.WaveSize() == 32u; + if ((lane & 15u) != 15u || state.laneCount != 1u || HostSubgroupSingleLane(state) || (!split && (state.splitSubgroup || state.hostSubgroupSize != state.program.WaveSize()))) return std::nullopt; + const auto stage = state.program.Resources().stage; + auto scan = MatchRowScan(inst.Argument(0), stage, false); + if (!scan) scan = MatchRowScan(inst.Argument(0), stage, true); + if (!scan) return std::nullopt; + const auto& capabilities = state.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) return std::nullopt; + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto identity = ConstantU32(state, scan->reduction->identity); + const auto row = Binary(state, spv::OpShiftRightLogical, u32, EmitSubgroupLocalInvocationId(state), ConstantU32(state, 4u)); + const auto inRow = Binary(state, spv::OpIEqual, boolean, row, ConstantU32(state, lane >> 4u)); + const auto source = ctx.Def(scan->source); + state.module.EmitCapability(spv::CapabilityGroupNonUniformArithmetic); + const auto reduce = [&](std::uint32_t include) { + const auto keys = Select(state, u32, include, source, identity); + const auto result = state.module.AllocateId(); + state.module.AddFunction(scan->reduction->reduce, u32, result, ConstantU32(state, spv::ScopeSubgroup), static_cast(spv::GroupOperationReduce), keys); + return result; + }; + if (!split) return reduce(inRow); + const auto upper = Binary(state, spv::OpUGreaterThanEqual, boolean, EmitHostSubgroupLane(state, ConstantU32(state, 0u)), ConstantU32(state, 32u)); + const auto lower = Unary(state, spv::OpLogicalNot, boolean, upper); + const auto lowerTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, lower)); + const auto upperTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, upper)); + return Select(state, u32, upper, upperTotal, lowerTotal); +} + } std::uint32_t EmitConvertU16U32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -969,7 +1033,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)); } @@ -1033,6 +1097,7 @@ std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { if (selector != nullptr && selector->HasImmediate() && state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u) { const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); if (const auto reduced = EmitHalfWaveReduction(ctx, inst, index)) return *reduced; + if (const auto reduced = EmitRowReduction(ctx, inst, index)) return *reduced; if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) { if (index >= 32u && UpperLaneWalk{state.program.Resources().stage}.ZeroValue(inst.Argument(0))) return ConstantU32(state, 0u); FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); @@ -1079,7 +1144,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 +1172,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/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..f30656653d 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"); } @@ -253,6 +260,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.float16DenormPreserve = target.float16DenormPreserve; + state.robustBufferAccess2 = target.robustBufferAccess2; state.hostSubgroupSize = target.subgroupSize; if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; @@ -262,23 +270,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 db0ca5d84b..9d60e3f054 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -391,13 +391,19 @@ constexpr std::array SrgbToLinear{ 0x3f700000u, 0x3f730000u, 0x3f750000u, 0x3f770000u, 0x3f790000u, 0x3f7b0000u, 0x3f7e0000u, 0x3f800000u, }; -std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { - const auto pointerType = TypePointer(state, spv::StorageClassPrivate, TypeU32(state)); +std::uint32_t SrgbTable(SpirvEmitterState& state) { + if (state.srgbTableVariable != 0u) return state.srgbTableVariable; std::array words{}; for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, SrgbToLinear[index]); const auto arrayType = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(words.size()))); - if (state.srgbTableVariable == 0) state.srgbTableVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); - state.module.AddFunction(spv::OpStore, state.srgbTableVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); + const auto table = state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words)); + state.srgbTableVariable = state.module.DefineInitializedGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate, table); + return state.srgbTableVariable; +} + +std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { + const auto pointerType = TypePointer(state, spv::StorageClassPrivate, TypeU32(state)); + const auto table = SrgbTable(state); std::uint32_t components[4] = {}; for (std::uint32_t index = 0; index < 4u; index++) { const auto value = state.module.AllocateId(); @@ -406,7 +412,7 @@ std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { const auto code = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), code, GlslStd450(state), GLSLstd450UMin, Unary(state, spv::OpConvertFToU, TypeU32(state), scaled), ConstantU32(state, 255u)); const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, state.srgbTableVariable, code); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, table, code); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), bits, pointer); components[index] = Unary(state, spv::OpBitcast, TypeF32(state), bits); @@ -741,9 +747,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 +769,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(); @@ -1468,7 +1471,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::UnnormalizedBaseLevelBase + mem.sampler, 0u); const auto unnormalized = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); sample = EmitValueIfElse(state, unnormalized, resultType, [&] { - return emitSample(setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); + return emitSample(hasOffset ? FoldedOffsetCoord(ctx, access, setup) : setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); }, [&] { return emitSample(setup.coord, operandMask, operands, opcode); }); @@ -1704,7 +1707,9 @@ TableAccessResult EmitTableAccess(SpirvValueEmitContext& ctx, const IrValue& ins result.flags = flags; live = selected; state.tableAccess.element = Binary(state, spv::OpIAdd, u32, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderElementBase)), element); - DecorateTableNonUniform(state, state.tableAccess.element); + const auto& sources = state.program.Resources().descriptorSources; + state.tableAccess.uniform = base.source < sources.size() && sources[base.source].tableColumn.has_value() && sources[base.source].tableColumn->selected; + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, state.tableAccess.element); state.tableAccess.dword3 = dword3; } if (tableSampler) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 9312b24e49..d208290cac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -20,7 +20,7 @@ namespace { if (ShaderWorkgroupInput(state) == nullptr) { FailEmit("function LDS was not prepared before function emission"); } - state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u)), spv::StorageClassWorkgroup); + state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsStorageDwords(state)), spv::StorageClassWorkgroup); state.module.AddName(state.ldsVariable, "lds_dwords"); } @@ -173,6 +173,26 @@ bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind) { return kind == ResourceKind::Lds && !LdsInDeviceMemory(state) && ShaderWorkgroupInput(state) == nullptr; } +std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value) { + return Select(state, TypeU32(state), condition, value, ConstantU32(state, 0u)); +} + +std::uint32_t LdsStorageDwords(const SpirvEmitterState& state) { + return LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u); +} + +bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access) { + if (access.kind == ResourceKind::Scratch) return true; + if (access.kind == ResourceKind::Lds) return false; + return state.robustBufferAccess2; +} + +std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index) { + if (access.kind == ResourceKind::Lds) FailEmit("LDS loads keep their branch and have no guarded element"); + const auto fallback = access.kind == ResourceKind::Scratch ? 0u : 0xfffffffeu; + return Select(state, TypeU32(state), condition, index, ConstantU32(state, fallback)); +} + std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 10a18de9ff..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; } @@ -156,10 +166,10 @@ std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t if (found == state.tableImageVariables.end() || !state.tableAccess.active) ExitDescriptorBindingFailure(state, kind, resource, "image 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); + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, pointer); const auto image = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, imageType, image, pointer); - DecorateTableNonUniform(state, image); + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, image); return image; } const auto arrayIndex = ResourceForDescriptor(state, kind, resource); @@ -206,7 +216,7 @@ std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, const auto sampledImage = state.module.AllocateId(); const auto sampledType = state.module.Type(spv::OpTypeSampledImage, ImageType(state, imageResource)); state.module.AddFunction(spv::OpSampledImage, sampledType, sampledImage, image, samplerId); - if (imageResource.table != NoTable || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage); + if ((imageResource.table != NoTable && !state.tableAccess.uniform) || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage); return sampledImage; } @@ -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..d561dea75c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -134,36 +134,35 @@ std::uint32_t LoadSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryResour return Binary(state, spv::OpShiftRightArithmetic, TypeU32(state), left, ConstantU32(state, 32u - bits)); } -std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource) { +std::uint32_t LoadEnable(SpirvEmitterState& state, const MemoryResourceAccess& resource, std::uint32_t index, std::uint32_t active) { + const auto inBounds = EmitMemoryElementInBounds(state, resource, index); + return active == 0u ? inBounds : Binary(state, spv::OpLogicalAnd, TypeBool(state), active, inBounds); +} + +std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t active) { auto& state = ctx.state; const auto index = EmitMemoryElementIndex(state, resource, DwordIndex(ctx, inst, mem)); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() { - return LoadWordInBounds(ctx, resource, index); - }); + const auto enable = LoadEnable(state, resource, index, active); + if (!LoadsSelectable(state, resource)) return EmitValueOrZeroIfCondition(state, enable, [&] { return LoadWordInBounds(ctx, resource, index); }); + return EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, GuardedElementIndex(state, resource, enable, index))); } std::uint32_t LoadWord(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { - return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - const auto resource = PrepareMemoryResourceAccess(ctx.state, mem); - return LoadWordPrepared(ctx, inst, mem, resource); - }); + return LoadWordPrepared(ctx, inst, mem, PrepareMemoryResourceAccess(ctx.state, mem), ActiveArgument(ctx, inst)); } -std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t bits, bool signExtend) { +std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t bits, bool signExtend, std::uint32_t active) { auto& state = ctx.state; const auto address = ByteAddress(ctx, inst, mem); const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); const auto index = EmitMemoryElementIndex(state, resource, rawIndex); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() { - return LoadSubwordInBounds(ctx, resource, address, index, bits, signExtend); - }); + const auto enable = LoadEnable(state, resource, index, active); + if (!LoadsSelectable(state, resource)) return EmitValueOrZeroIfCondition(state, enable, [&] { return LoadSubwordInBounds(ctx, resource, address, index, bits, signExtend); }); + return EmitValueOrZero(state, enable, LoadSubwordInBounds(ctx, resource, address, GuardedElementIndex(state, resource, enable, index), bits, signExtend)); } std::uint32_t LoadSubword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { - return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - const auto resource = PrepareMemoryResourceAccess(ctx.state, mem); - return LoadSubwordPrepared(ctx, inst, mem, resource, bits, false); - }); + return LoadSubwordPrepared(ctx, inst, mem, PrepareMemoryResourceAccess(ctx.state, mem), bits, false, ActiveArgument(ctx, inst)); } std::uint32_t ConstantDeviceAddress(SpirvEmitterState& state, std::uint64_t value) { @@ -368,20 +367,19 @@ std::uint32_t ConstructU32Composite(SpirvEmitterState& state, std::uint32_t comp std::uint32_t LoadWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { auto& state = ctx.state; - return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() { - const auto resource = PrepareMemoryResourceAccess(state, mem); - const auto base = ByteAddress(ctx, inst, mem); - std::array values{}; - for (std::uint32_t component = 0; component < components; component++) { - const auto address = component == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, component * 4u)); - const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); - const auto index = EmitMemoryElementIndex(state, resource, rawIndex); - values.at(component) = EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() { - return LoadWordInBounds(ctx, resource, index); - }); - } - return ConstructU32Composite(state, components, values); - }); + const auto active = ActiveArgument(ctx, inst); + const auto resource = PrepareMemoryResourceAccess(state, mem); + const auto base = ByteAddress(ctx, inst, mem); + std::array values{}; + for (std::uint32_t component = 0; component < components; component++) { + const auto address = component == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, component * 4u)); + const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + const auto index = EmitMemoryElementIndex(state, resource, rawIndex); + const auto enable = LoadEnable(state, resource, index, active); + values.at(component) = LoadsSelectable(state, resource) ? EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, GuardedElementIndex(state, resource, enable, index))) + : EmitValueOrZeroIfCondition(state, enable, [&] { return LoadWordInBounds(ctx, resource, index); }); + } + return ConstructU32Composite(state, components, values); } void StoreWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { @@ -1365,7 +1363,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 78de6340b4..763f1a1fa8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -240,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); } @@ -343,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 774e16aa09..accf51eb13 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"); @@ -254,8 +254,8 @@ std::uint32_t MrtOutputMode(const SpirvEmitterState& state, const ExportInfo& ex return state.inputInfo.pixel->targetOutputMode[exp.index]; } -std::uint32_t PackUnorm10_11_11(SpirvEmitterState& state, std::uint32_t color) { - const auto info = GetFormatInfo(IrBufferFormat::Format11_11_10UNorm); +std::uint32_t PackExport(SpirvEmitterState& state, IrBufferFormat format, std::uint32_t color) { + const auto info = GetFormatInfo(format); auto packed = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < info.componentCount; component++) { const auto value = state.module.AllocateId(); @@ -270,8 +270,9 @@ std::uint32_t PackUnorm10_11_11(SpirvEmitterState& state, std::uint32_t color) { std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target, std::uint32_t color) { const auto packing = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + target, static_cast(ColorExportPacking::None)); - const auto packed = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Unorm10_11_11)); - const auto word = PackUnorm10_11_11(state, color); + const auto packed = EmitCompareU32Constant(state, spv::OpINotEqual, packing, static_cast(ColorExportPacking::None)); + const auto floating = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Float11_11_10)); + const auto word = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { const auto unpacked = state.module.AllocateId(); @@ -767,11 +768,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 +838,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)); @@ -1146,7 +1147,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.entryLabel); if (state.requirements.functionLds) { - state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsDwordCount(state)), state.ldsVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsStorageDwords(state)), state.ldsVariable, spv::StorageClassFunction); } if (state.requirements.functionScratch) { for (std::uint32_t half = 0; half < state.laneCount; half++) { 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::span values{}; for (std::uint32_t component = 0; component < components; ++component) { const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); - values[component] = EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); }); + values[component] = memory.gpuDescriptor ? EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); }) + : readBuffer(context, instruction, access.guest, memory.dataBits, access.inBounds); } return composite(state, components, values); }; @@ -776,7 +788,7 @@ std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const const auto byte = EmitAndConstant(state, address, ~3u); const auto offset = Unary(state, spv::OpUConvert, u64, byte); const auto inBounds = both(state, noCarry, both(state, resource.valid, Binary(state, spv::OpULessThanEqual, TypeBool(state), Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, 4u)), size))); - if (!context.Memory(instruction).gpuDescriptor) return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, byte, 32u); }); + if (!context.Memory(instruction).gpuDescriptor) return readBuffer(context, instruction, byte, 32u, inBounds); return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, AddBdaAddress(context, instruction, resource.base, offset, false), 32u); }); } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index c174855b88..4b0f5b94dc 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -61,6 +61,7 @@ private: bool outputModifierApplies(std::uint32_t denormalShift) const; bool dx10Clamp() const { return !floatMode.has_value() || floatMode->dx10Clamp; } 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 54b4476de1..60e568cd98 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,17 +111,38 @@ 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 a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), 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, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. 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 A T# or S# that a shader loads from a buffer at a runtime offset (`key * stride + addend`, or `(key << n) & mask`) is not a direct descriptor. [`ResourceTracker.cpp`](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans it as a table column (`TableColumn`): the heap V#'s descriptor source, the record stride and addend, the descriptor's offset in the record, and the key domains that may narrow it. Accesses with the same heap and record shape share one column. The shader keeps computing the offset and indexes a table binding: images use one variable-count `SAMPLED_IMAGE` array (`RuntimeAbi::ImageTableBinding`, in the fragment set when pipeline libraries split the sets), samplers a `SAMPLER` array (`Binding::SamplerTable`). Devices without `runtimeDescriptorArray`, `descriptorBindingPartiallyBound` and `descriptorBindingVariableDescriptorCount` refuse such shaders. -At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A column narrows to the records its keys select, plus record 0, when every access has a key domain: a value the SRT walker evaluates from user data, SRT reads and constants, or a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table changes neither the SPIR-V nor the variant's disk key. +A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler. + +At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A selected column evaluates its offset and reads only that record, as the scalar loads would: dwords past the heap read zeros, an unmapped record or an offset that carries past 32 bits is poison, and an offset the walker cannot read faults the table instead of throwing. Its heap base moves to the record, so the shader's fault address and the written-heap check cover that record alone. Any other column narrows to the records its keys select, plus record 0, when every access has a key domain: a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`), or a value the SRT walker evaluates from user data, SRT reads and constants (which, for an image, selects its record before it could narrow). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table or key changes neither the SPIR-V nor the variant's disk key. `ResolveImageTables` turns the snapshot into the map buffer (`DescriptorRole::ImageTableMap`, layout in [`ImageTableAbi.hpp`](../../core/shader/recompiler/ImageTableAbi.hpp)): a header, eight words per table (map start, key count, heap base and size, the code for keys past the snapshot, a table fault) and one record per key (flags with the decode mode, the swizzle dword and the element for images; flags and element for samplers). The shader reads `map[key]` and switches over the image's prepared runtime modes. Each distinct T# becomes one element, up to `ImageElementBudget` and the device's capacity. An entry that does not decode, is multisampled or over budget, pairs a reducing sampler with a point-only mode, is an unnormalized or reducing S#, or whose surface has no mapped byte is poisoned: it reads zeros and reports a fault through the BDA fault buffer only when a lane selects it (`APS5_IMAGE_TABLE_STRICT=1` throws at the snapshot instead). The driver's `resolveImageTables` in [`ShaderResources.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) looks up or creates a texture per element and keeps the last four resolved tables per thread for reuse within one collect epoch while their memory is unchanged. Cached objects grow with their tables, so the resource cache keeps at most `ResourceCache::TableElementBudget()` table elements (65,536, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) and evicts its least recently used table objects past that, and the dispatch cache evicts its least recently used entries while their captured words exceed `Driver::DispatchCacheBytes()` (256 MiB, `APS5_DISPATCH_CACHE_BYTES`). -The disk cache (`FormatVersion` 33) stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains. +The disk cache stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains and its selection. ### Untracked storage image comparisons @@ -134,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..71dcccda64 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. | @@ -149,7 +151,7 @@ Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in p Set `APS5_TRACE_FRAMES=:` to record `` presents from present `` (counted from 0), or `APS5_TRACE_SECONDS=:` to record from the first present at least `` seconds after the first present until the first present `` seconds later. Both values are whole numbers and the length is above 0; a malformed value, or both switches together, fails the first present. The window opens and closes at presents. -Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, and the GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. +Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. @@ -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 237a10e00d..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, 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 5662ba6a3e..e67564161c 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,35 +17,44 @@ - [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. -- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. +- [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. +- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). @@ -56,21 +71,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. -- [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 +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled +- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [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 +103,8 @@ - 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. +- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. 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. @@ -93,7 +112,7 @@ - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page -- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). +- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a 3D or array color target throw; base mip 0 of a 2D mipmapped color target is supported (offset follows AMD Addrlib `Gfx10Lib::HwlComputeCmaskInfo` reverse mip-chain ordering preceding base mip 0, unverified on PS5 hardware; mipmapped targets with 8 or 16 bpp throw until `dsMipmapHtileFix` mip-tail shrinkage is modelled). A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), so such a slice takes the clear only when all its words are equal and otherwise keeps the fill-time fast clears. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. @@ -104,9 +123,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 +136,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,17 +148,18 @@ - [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. - [Table S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) DISABLE_CUBE_WRAP faults on selection without `VK_EXT_non_seamless_cube_map`: no runtime dimension check, including valid non-cube direct/table T#s. Unselected entries do not fault; with the feature this bit alone is accepted. Table T#/direct S# faults only for cubes needing the missing feature. -- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. +- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block, calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. @@ -151,3 +173,6 @@ - [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. - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. +- [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. + +- [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. 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..a9c31f321e 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -4,9 +4,10 @@ - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [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/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..050b0f7c9d 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -2,15 +2,17 @@ ### Silent stubs -- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` fails, and every other export it has (and every [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) export) throws. PPSA01317 skips its whole VR set-up when initialization fails +- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reports a console without a PS VR2 headset, as firmware 13.20's libSceHmd2 does with the `/dev/hmd2_cmd` state measured on a retail PS5 with none connected (connected flag 0, status 2, user -1): initialization succeeds, `sceHmd2Open` returns a handle, device information reports `NotDetected` with the firmware's fixed panel, latency and image distance values, both field-of-view calls return the firmware's default tangents and `sceHmd2SetVibration` succeeds without vibrating. The reprojection and gaze exports and every [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) export throw. PPSA01317, PPSA07411, PPSA09030, PPSA15878 and PPSA29866, which skipped their VR set-up while initialization failed, have not been run with initialization succeeding ### Unknown function info - PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) -- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero -- PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list +- 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) +- [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use +- [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal +- PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list ### Functional -- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2GetDeviceInformationByHandle, sceHmd2GetFieldOfView and sceHmd2Terminate](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled 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/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 023da38c79..123977e935 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,4 +2,4 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run +- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. `shutdown` throws while a compute sequence has work in flight, a sequence's label total is written as 32 bits, and since the label is host memory no GPU import covers, the driver drains recorded work before each label write. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run 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/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7cc67d0d95..c59a80336b 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -14,4 +14,6 @@ - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified +- [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read +- [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws 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 511522085f..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. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4. +- [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 c7b654e7b0..8076229ee3 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -3,6 +3,8 @@ ### 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 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..33aa1abb92 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -2,4 +2,18 @@ ### 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). +- [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as the callback's signature and when the console calls it are not known, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). + +### Unknown function info + +- [Query structures](../../../core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp): a progress value is a 16-byte `{int32 type, 4 reserved bytes, uint64 value}` and the game, group and trophy structures are 152, 24, 152, 32, 1312 and 32 bytes, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) asserts; no other public source confirms the layout. Progress type 1 marks a progressive trophy's target and progress, as in KytyPS5. Trophies without a `groupId` belong to group -1, KytyPS5's base game group. + +- [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 + +### Functional + +- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. +- [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. +- [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. +- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. +- [Damaged records](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp): a line in `_trophy/_.txt` or `.stats.txt` that is not a new ` ` pair (a blank line, a carriage return, a repeated or negative id, a value that is not a number) makes `readRecord` throw a `std::runtime_error` that names the file and the line. Every call that reads that record then throws it instead of returning, which stops the title as any throwing export does, so the title gets no return value; main returned 0 from these calls. The failed read is not kept, so each later call reads the file again and throws again until the file is fixed or deleted. The unlock record is read by `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` for a created context, by `sceNpTrophy2GetGroupInfo` and `sceNpTrophy2GetGroupInfoArray` when the title's trophy data is loaded, by a `sceNpUniversalDataSystemPostEvent` of an `_UnlockTrophy` that the trophy data does not refuse, and by a statistic change when the trophy data is loaded; the stat record by a `sceNpUniversalDataSystemPostEvent` of an event that a statistics rule copies and by the trophy queries of a progressive trophy with a statistic. What the console does with a damaged record is not known. diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md new file mode 100644 index 0000000000..6c8a996278 --- /dev/null +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -0,0 +1,10 @@ +# libSceNpUniversalDataSystem technical debt + +### Unknown function info + +- [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). + +### Functional + +- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does, except an id that [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp) refuses when the title's trophy data is loaded (one not in the data, a platinum or a progressive trophy). `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. +- [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only statistics with `"aggregation":"latest"` and `dataType` `uint32` or `uint64` are kept, as in KytyPS5; no public source documents the other aggregations or types, so their definitions and rules are skipped. Only rules whose condition is an `eventName` alone and whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. 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..6ec836b141 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,10 +2,13 @@ ### 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 +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown +- [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/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md new file mode 100644 index 0000000000..c8920961b9 --- /dev/null +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -0,0 +1,5 @@ +# libScePlayerSelectionDialog technical debt + +### Silent stubs + +- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `Close`, `GetStatus`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) diff --git a/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md index 027e2ab7dc..cf1e21965c 100644 --- a/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md +++ b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceProprietaryVoiceChatHelperPrx](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) implements only the no-system-voice-chat path: the host has no system voice chat for a title's own voice chat to announce itself to, so `sceProprietaryVoiceChatHelperInitialize` and `sceProprietaryVoiceChatHelperTerminate` only track whether the library is initialized, and `sceProprietaryVoiceChatHelperSetVoiceChatState` returns success and does nothing: there is nothing to notify, and its state argument is unknown, so no state is recorded. `sceProprietaryVoiceChatHelperGetVoiceChatUsageState` throws, because nothing it could report is known: with the state argument unreadable there is no recorded usage state, and whether the console returns the state or writes it through an output pointer, and how it encodes it, is unknown. Initializing twice, and any call before `sceProprietaryVoiceChatHelperInitialize` or after `sceProprietaryVoiceChatHelperTerminate`, throws, as the console's error codes for this library are unknown. Whether the console checks initialization order at all is unverified +- [libSceProprietaryVoiceChatHelperPrx](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) implements only the no-system-voice-chat path: the host has no system voice chat for a title's own voice chat to announce itself to, so `sceProprietaryVoiceChatHelperInitialize` and `sceProprietaryVoiceChatHelperTerminate` only track whether the library is initialized, and `sceProprietaryVoiceChatHelperSetVoiceChatState` returns success and does nothing: there is nothing to notify, and its state argument is unknown, so no state is recorded. `sceProprietaryVoiceChatHelperGetVoiceChat` and `sceProprietaryVoiceChatHelperGetVoiceChatUsageState` throw, because nothing they could report is known: with the state argument unreadable there is no recorded usage state, and whether the console returns the state or writes it through an output pointer, and how it encodes it, is unknown. Initializing twice, and any call before `sceProprietaryVoiceChatHelperInitialize` or after `sceProprietaryVoiceChatHelperTerminate`, throws, as the console's error codes for this library are unknown. Whether the console checks initialization order at all is unverified diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 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 c12f2befd8..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 @@ -16,4 +17,5 @@ - [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..fc94a535ec 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,12 +2,8 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts -- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [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) +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection` and that `sceSslRead` returns 0 at the end of the stream. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: SharpProspero's `SceSslVersion` ends at TLS 1.2, and Mbed TLS 1.3 needs PSA crypto state that is process-wide rather than allocated from a context's pool. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`. The server is verified against the CAs loaded with `sceSslLoadCert`: chain, validity dates and the host name the connection was created with, which is also sent as SNI. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. With no CA loaded `sceSslConnect` throws, because no root bundle is shipped and the console's built-in roots are unknown. With a verify callback set it throws: SharpProspero describes the arguments as an id, bits saying what failed, the chain, its length and the user argument, with a negative return failing the handshake with that value, but whether the id is the context or the connection, the bit values, the chain element type and when the callback runs are unknown. Handshake, verification and socket failures, a second `sceSslConnect`, a read or write before the handshake, a descriptor that is not a socket, a null buffer, write flags other than 0 and read flags other than 0 and `MsgFlagPartialRead` (1) throw, as their console codes are unknown; `MsgFlagPeek` (3) is not implemented. A non-blocking socket that would block throws, and so does a libSceNet socket whose `SO_RCVTIMEO` or `SO_SNDTIMEO` expires, as libSceNet sets them on the host socket; libkernel keeps them above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` returns what one Mbed TLS read delivers, up to `len`, with flags 0 and with `MsgFlagPartialRead` alike; SharpProspero describes `sceSslRead` as reading up to `len` but `MsgFlagPartialRead` as returning before the buffer is full, so whether flags 0 waits to fill the buffer is unknown. It returns 0 after the server's close_notify and also when the transport ends without one. `sceSslWrite` sends all of `len`, capped at `INT_MAX`, and returns the count. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. None of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection ID (`int`) and throws `NotImplemented_nid_no_patch(__func__)` until there is a transport that closes it. +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - 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, 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 the protocol negotiated by the handshake is not read. 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/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 085b02a1a2..f159b8f893 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -8,3 +8,4 @@ - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetAccessibilityKeyremapEnable, sceUserServiceGetAccessibilityZoom](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - declared without parameters as throwing stubs calling `NotImplemented_nid_no_patch`. Their signatures and values remain unverified on hardware 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 50359fc84c..d3ac2a4d01 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. @@ -12,9 +15,11 @@ - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error +- [sceLibcPafMspaceCreate, Destroy, Malloc, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5D680 to 0x5D770): each calls the same internal routine as its `sceLibcMspace` counterpart without the malloc callback hook those add, and `Reallocalign` takes the alignment before the size as `sceLibcMspaceReallocalign` does. `Create` takes a fifth argument, a pointer to a creation options block (a version word, a type of 1 or 2 at offset 2, a field at 0x10) that the `Mspace` form passes as null; a non-null block throws, as its meaning is unknown. `Malloc`, `Free`, `Calloc`, `Realloc` and `Reallocalign` take a trailing owner value that the console XORs with its block magic and stores in each block's footer, where the `Mspace` forms pass the heap itself. Footers are not modelled here, so the owner is ignored. Measured on a PS5 (firmware 13.60) from a homebrew payload, the calls with a matching owner behave as here, but the console ends the process when `Reallocalign` is given a block whose owner differs and when it resizes a block in place with an owner other than the heap, and `MallocStats` returns 1 instead of 0 while the heap holds blocks owned by something other than the heap; none of this is reproduced. `PafMspaceMemalign`, `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol @@ -24,6 +29,8 @@ - [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. @@ -31,23 +38,31 @@ - [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, 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 +- [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [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 +- [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws +- [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed +- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ. As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [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 +- [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - 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; @@ -61,8 +76,9 @@ - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached +- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments 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 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 as the guest's bytes and sizes the result in bytes (two per character, a 16-bit terminator, the 7 characters that still fit next to it and `length * 2 | 15` plus two bytes of storage beyond that), 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. +- [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` @@ -73,9 +89,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 fe0bab9ad7..67b2f5ae3c 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` @@ -34,7 +33,9 @@ - [AMM remap, multimap and protection changes](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths -- [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signature +- [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `32KQRUK13kI`) - `(uint32_t, const char* text, int)` follows the console's `libkernel.sprx` (SDK 4.5x build of `J03950001`) at 0x13050: a null `text` returns `0x81180000`; otherwise the three arguments go to syscall 601 (command 10), whose `EPERM` becomes `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003), `EINVAL` `0x81180000`, and a success returns the value the kernel wrote back. Measured on a PS5 (firmware 13.60) from a homebrew payload with no core dump handler: the call succeeds with 0, the kernel rejects a first argument above 15 or a third outside 0 to 256 with `0x81180000`, and a 2047-character text is accepted. Those checks are reproduced and anything else returns 0; nothing is recorded, as there is no dump, and what the two numbers mean is unknown. PPSA16064 imports it +- [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - `(int mode)` follows the same `libkernel.sprx` at 0x134D0: a mode other than 1 returns `0x81180000`; mode 1 issues syscall 601 (command 16, arguments 0 and 1) and returns 0 whether or not it fails, since only a raw return of 22 would map to `0x81180000`. No dump is produced here, so mode 1 returns 0 without the syscall. PPSA16833 imports it +- [sceCoredumpGetStopInfoGpu_Agc](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Jrs7UUkGOFo`) - `(void* info, size_t size)` follows the same `libkernel.sprx` at 0x137E0: a null `info` returns `0x81180000`; syscall 601 (command 21) failing with `EPERM` returns `NOT_IN_COREDUMP_HANDLER`; otherwise two `libSceAgcDriver` imports (NIDs `+NIAY3DWpOw` and `LgctzJSGy7s`, names unknown) decide between `0x81180005` and, for a `size` of 8, storing the second one's 64-bit result and returning 0, with any other size returning `0x81180000`. The handler is never invoked here, so a non-null `info` returns `NOT_IN_COREDUMP_HANDLER` without writing, assuming `EPERM` outside the handler as above; the AGC branch is not reached - [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 +48,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 @@ -64,15 +65,15 @@ - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified -- [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,32 +105,33 @@ ### 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 - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target, a junction and any other reparse point throw, as for `readlink` -- [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 +- [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified -- [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. 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_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 @@ -143,23 +144,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 @@ -167,7 +171,8 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A 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 +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. - [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. `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 @@ -175,13 +180,14 @@ - [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, 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`. 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 +- [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. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. 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. @@ -189,7 +195,8 @@ - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields -- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever +- [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. @@ -200,3 +207,4 @@ - [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 9cd0b0d215..2a8365c5d3 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -9,7 +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. +- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - 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. @@ -17,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 c75e3eb450..c567f99417 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -31,17 +31,18 @@ - [Image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures are decoded by `image_load` like a direct T#; a sample, gather or LOD query of one faults (`sRGB decode`) on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (T#s and S#s that `s_buffer_load` reads at `key * stride + addend` from a V#) are snapshotted when the dispatch or draw is recorded; a later CPU write to a record is not seen. - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates from user data, SRT reads and constants alone (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask`: a key domain would select records by the unmasked key, so that key is not narrowed. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. -- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. +- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - An image-table T#/S# pair with anisotropic `ANISO_OVERRIDE` faults on selection: dynamic mip-level pairing is unimplemented, including otherwise-valid single-level and mipmapped tables. Direct pairs keep their override behavior. Unselected entries, anisotropy without the override and the override without anisotropy stay supported. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. - Comparison sampling of a color texture through an [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# throws at the first dispatch or draw that binds it. - An [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) key with no key domain whose `key * stride` wraps past 2^32 to an offset between records faults only when the build already expects a fault; otherwise the fault buffer is not read and it reads zeros. - [MaskedSelectEliminator](../../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. +- [DenormalFlushEliminator](../../../core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp) drops the result flush of a `v_mul_f32` or `v_mul_legacy_f32` product behind its nearest-even tininess check (see f32 denormals) when both factors are denormal-free: a product the check keeps is then zero, infinite, NaN or at least 2^-126 in magnitude, provided the driver rounds it as it rounds the check's multiply. - [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [SPIRV-Tools optimization](../../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. @@ -70,7 +71,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 +84,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. -- 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_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, 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. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [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 +123,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 +132,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`. @@ -136,7 +144,7 @@ - [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. - The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) -- [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. +- [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated barycentric geometry stage do not write it, the generated rect-list and barycentric tessellation stages forward the vertex shader's array, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) hold the pixel-centre barycentrics, like the centroid pairs. With one sample this is the sample position ([Vulkan standard sample locations](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-multisampling): (0.5, 0.5)); with more samples the console's per-sample weights are not computed and were not measured. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair 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. @@ -146,3 +154,5 @@ - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. +- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the high half of a wave64 skipped its loop. The cause was not found. 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..27b1862066 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -8,7 +8,7 @@ The online converter can save Linux output as an uncompressed folder in supporte ## Input and conversion -Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text source/ @@ -21,6 +21,8 @@ source/ relinker [options] ``` +An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. + Linux output: ```sh @@ -60,7 +62,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,13 +109,15 @@ 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. On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. -On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the range does not start on the device's sparse block size in the backing file, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. +On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the device's sparse block size is larger than 64 KiB, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. Linux: @@ -142,9 +146,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 +159,29 @@ 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. +### Trophies + +A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`, or through the statistics rules described below. The unlock is stored in `_trophy/_.txt` in the directory the game runs in (the one that holds `app0/`), one ` ` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. When the game's trophy data is loaded (see below), a posted unlock of a trophy that the data does not have, of the platinum, or of a progressive trophy is not stored and prints the reason, such as `[trophy] ignored the posted unlock of trophy 0 for user 268435456: it is the platinum, which unlocks with the last trophy that names it`; the platinum unlocks with the last trophy that names it, and a progressive trophy through its statistic. Without trophy data every posted id of 0 or more is stored, and the platinum is not unlocked when the other trophies are. + +The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. A platinum trophy unlocks as soon as every trophy that names it in `platinumTrophyId` is unlocked. The first trophy query or event prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. + +Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/_.stats.txt`, one ` ` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest value (`"aggregation":"latest"`) as `uint32` or `uint64`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), are applied; the others are listed in `[trophy]` lines when the file is read. + +`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; the first `_UpdateTrophyProgress` event of each trophy, because that event is not interpreted; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. + +A missing or malformed `trophyNN.ucp` or `udsNN.ucp` turns off only what it provides, and a malformed one prints the reason once: without trophy data the queries report placeholders and statistics unlock nothing, and without statistics rules events change no statistic. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock or statistics file in `_trophy/` that is damaged (a line that is not a new ` ` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. + +### 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 +205,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 +238,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 +276,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. | @@ -238,6 +286,8 @@ The report also lists needed libraries that have no file in `--libs` or `--modul | `--names ` | NID and name per line, as in `aerolib.csv` from [`tools/nid_names.py`](../../tools/nid_names.py); names the absent imports. Never downloaded; a name that does not hash to its NID is marked. | | `--json ` | Full result, stamped with the repository's commit and whether its tree was modified. | +Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol names such as `strcmp`. Imports are matched against literal exports first, then against their computed NIDs when the literal lookup misses. Reports retain the original import name. Export availability does not establish that every platform loader supports that lookup path. + Exit codes: `0` nothing blocks loading, `1` there are `absent` imports or needed libraries without a file, `2` an input could not be read; the message names the file and the value. A module registry lists `nid`, `library` and `targetOffset` for each relocation against a function that no other bundled module exports, without the `#` suffix of the executable's NIDs. diff --git a/tools/build_web.py b/tools/build_web.py index 0d8bb92408..cd19113603 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): @@ -62,18 +63,24 @@ def publish_libraries(output): files = archive.infolist() if not files or len(files) > 4096: raise ValueError("Invalid library archive") - names = set() + names = {} for file in files: if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename: raise ValueError("Invalid library archive layout") name = file.filename.rstrip("/").lower() if name in names: raise ValueError(f"Duplicate library archive path: {file.filename}") - names.add(name) + names[name] = file.is_dir() + for name in names: + parts = name.split("/") + if any(names.get("/".join(parts[:index])) is False for index in range(1, len(parts))): + raise ValueError(f"Library archive file is also used as a directory: {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..47076b046b 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,31 +34,36 @@ 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 def changed(root, base): - result = capture(["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", "-z", base]) + fork = capture(["git", "-C", str(root), "merge-base", base, "HEAD"]) + if fork.returncode != 0: + raise ValueError(f"git merge-base {base}: {fork.stderr.strip()}") + result = capture(["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", "-z", fork.stdout.strip()]) if result.returncode != 0: raise ValueError(f"git diff {base}: {result.stderr.strip()}") untracked = capture(["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "-z"]) @@ -68,9 +73,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 +111,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,28 +128,36 @@ 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 def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") parser = argparse.ArgumentParser( description="compile the changed translation units only, to check a change builds without a full build") parser.add_argument("--build", type=Path, default=Path("build"), diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..a5dade7483 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -1,10 +1,12 @@ import argparse +import io import json import os import posixpath import re import subprocess import sys +import tokenize from collections import defaultdict from pathlib import Path, PurePosixPath from urllib.parse import unquote @@ -20,7 +22,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"), @@ -43,6 +45,7 @@ DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') ALLOWED_COMMENT = re.compile(r"^\s*(#\s*endif\b|}\s*;?\s*//\s*(end\s+)?namespace\b)") +CMAKE_TOKEN = re.compile(r'\[(=*)\[.*?\]\1\]|"(?:\\.|[^"\\])*"|(?P#\[(?P=*)\[.*?\](?P=level)\]|#[^\n]*)|\\.', re.S) EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s*;)*" r"(?:return\s*\(?\s*(?:-?(?:0x[0-9a-fA-F]+|\d+)[uUlL]*|nullptr|true|false|\w*_OK)?\s*\)?\s*;)?$") @@ -50,7 +53,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*$") @@ -159,9 +162,12 @@ class Check: self.report("silent-stub", path, number, match.group(1)) self.duplicates(exports) for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): + source = self.show(path) + comments = python_comments(source) if path.endswith(".py") else cmake_comments(source) for number, text in lines: shebang = text.startswith("#!") or (number == 1 and text.startswith("\ufeff#!")) - if "#" in STRINGS.sub('""', text) and not shebang: + comment = number in comments if comments is not None else "#" in STRINGS.sub('""', text) + if comment and not shebang: self.report("comment", path, number) if any(is_debt(path) for path in self.status): self.findings = [finding for finding in self.findings if finding[0] != "comment"] @@ -170,13 +176,20 @@ 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) + sources = {} for path, number, text in hits: - name = EXPORT.search(text).group(1) + if path not in sources: + sources[path] = cpp_text(self.show(path)).split("\n") + source = sources[path] + match = next((match for match in EXPORT.finditer(source[int(number) - 1]) if match.group(1) in exports), None) + if match is None: + continue + name = match.group(1) own, own_number = exports[name] - if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(self.show(path).splitlines(), int(number) - 1) is not None: + if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(source, int(number) - 1) is not None: self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def build(self): @@ -372,7 +385,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,12 +408,29 @@ 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")) +def python_comments(text): + try: + tokens = list(tokenize.generate_tokens(io.StringIO(text.removeprefix("\ufeff")).readline)) + except (tokenize.TokenError, SyntaxError): + return None + return {token.start[0] for token in tokens if token.type == tokenize.COMMENT} + + +def cmake_comments(text): + lines = set() + for match in CMAKE_TOKEN.finditer(text): + if match.group("comment") is not None: + first = text.count("\n", 0, match.start()) + 1 + lines.update(range(first, first + match.group().count("\n") + 1)) + return lines + + def cpp_text(text, preserve_comments=False): def mask(match): if match.group("code") is not None or preserve_comments and match.group("comment") is not None: diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index fa3265d310..12d5b78a32 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -97,12 +97,17 @@ if __name__ == "__main__": parser.add_argument("--strict", action="store_true", help="also fail when the branch is behind the base") args = parser.parse_args() try: - base = resolve_base(args.repo, args.base) - if args.fetch: - remote, _, branch = base.partition("/") + def fetch(ref): + remote, _, branch = ref.partition("/") if not branch: parser.error("--fetch needs a remote-tracking base such as upstream/main") git(args.repo, "fetch", "--quiet", remote, branch) + + if args.fetch and args.base: + fetch(args.base) + base = resolve_base(args.repo, args.base) + if args.fetch and not args.base: + fetch(base) data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) diff --git a/tools/check_skips.py b/tools/check_skips.py index 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..6b1fc4dddd 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -44,7 +44,9 @@ def oracle(): root = rocm_root() if root is not None: root = root.resolve() - configuration = repr((compiler, str(root))).encode("utf-8") + executable = shutil.which(compiler[0]) + selected = str(Path(executable).resolve()) if executable is not None else None + configuration = repr((compiler, selected, str(root))).encode("utf-8") key = hashlib.sha256(source.read_bytes() + configuration).hexdigest() directory = CACHE / key binary = directory / "oracle" @@ -65,12 +67,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): @@ -94,7 +102,7 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 return work / "k.co" -def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): +def validated_rows(rows): validated = [] for index, row in enumerate(rows, 1): try: @@ -104,7 +112,11 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de if len(values) != 4 or any(value < 0 or value > 0xffffffff for value in values): raise ValueError(f"row {index} needs 4 unsigned 32-bit integers") validated.append(values) - rows = validated + return validated + + +def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): + rows = validated_rows(rows) wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) @@ -120,8 +132,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)] @@ -151,11 +167,15 @@ def main(): parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") parser.add_argument("--json", action="store_true", help="emit inputs, results and run settings as JSON; target is the assembly target, not a GPU model or driver") 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 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"" - body = args.body.read_text() + try: + rows = validated_rows(tuple(parse_word(v) for v in line.split()) + for line in args.rows.read_text(encoding="utf-8").splitlines() if line.strip()) + if not rows: + parser.error("rows file must contain at least one input row") + extra = args.extra.read_bytes() if args.extra else b"" + body = args.body.read_text(encoding="utf-8") + except (OSError, ValueError) as error: + parser.error(str(error)) modes = {name: getattr(args, name) for name in ("ieee", "denorm32", "denorm16", "dx10_clamp", "round32", "round16", "fp16_overflow")} results = run(body, rows, extra, args.wave64, args.coarse, **modes, lds=args.lds) if args.json: diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 31fa5240e0..322e00a861 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -79,6 +79,59 @@ class OracleCacheTests(unittest.TestCase): class InputRowsTests(unittest.TestCase): + def test_cli_reports_invalid_input_before_tool_lookup(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)] + argv += [part for name, value in MODES.items() + for part in ["--" + name.replace("_", "-"), str(value)]] + for contents in (None, b"not a number\n", b"1 2 3\n", b"-1 0 0 0\n", b"4294967296 0 0 0\n", b"\xff"): + with self.subTest(contents=contents): + rows.unlink(missing_ok=True) + if contents is not None: + rows.write_bytes(contents) + with patch("sys.argv", argv), patch.object(hw_oracle, "assemble") as assemble, \ + patch.object(hw_oracle, "oracle") as oracle, \ + 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("error:", errors.getvalue()) + self.assertEqual(output.getvalue(), "") + assemble.assert_not_called() + oracle.assert_not_called() + + 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 +140,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): @@ -206,8 +281,9 @@ class JsonReportTests(unittest.TestCase): self.rows.write_text("0x100000000 0 0 0\n", encoding="utf-8") output = io.StringIO() with patch.object(hw_oracle, "target", side_effect=AssertionError("target lookup")) as target: - with self.assertRaisesRegex(ValueError, "row 1"): + with self.assertRaises(SystemExit) as exited: self.cli("--json", output=output) + self.assertEqual(exited.exception.code, 2) target.assert_not_called() self.assertEqual(output.getvalue(), "") @@ -279,6 +355,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")), \ @@ -299,6 +408,30 @@ class TargetOverrideTests(unittest.TestCase): class OracleConfigurationTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "POSIX executable compiler fixture") + def test_path_selecting_another_compiler_invalidates_the_cache(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in ("first", "second"): + compiler = root / name / "fixture-cc" + compiler.parent.mkdir() + compiler.write_text( + "#!" + sys.executable + "\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[sys.argv.index('-o') + 1]).write_bytes(" + repr(name.encode()) + ")\n", + encoding="utf-8") + compiler.chmod(0o755) + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=None): + with patch.dict(os.environ, {"CC": "fixture-cc", "PATH": str(root / "first")}): + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), b"first") + self.assertEqual(hw_oracle.oracle(), first) + with patch.dict(os.environ, {"CC": "fixture-cc", "PATH": str(root / "second")}): + second = hw_oracle.oracle() + self.assertEqual(second.read_bytes(), b"second") + self.assertNotEqual(first, second) + + @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") def test_cache_and_compiler_command_with_host_toolchain(self): compiler = shutil.which("cc") diff --git a/tools/import_audit.py b/tools/import_audit.py index 037dba8fe3..cb5f9ac8b4 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,7 +1,6 @@ import argparse import hashlib import json -import re import struct import subprocess import sys @@ -14,7 +13,6 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" -NID = re.compile(r"[A-Za-z0-9+\-]{11}") NID_POSTFIX = "_nid_postfix" MODULE_SUFFIX = "#" CLASSES = ("implemented", "stub", "absent", "module") @@ -182,7 +180,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 +218,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 +675,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 +710,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 +734,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 6a558f828d..87d5010e9f 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*\(") @@ -17,12 +18,16 @@ FETCH_REFS = 100 def git(*args): - return subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True).stdout + result = subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True) + result.stdout = result.stdout.decode("utf-8", errors="replace") + result.stderr = result.stderr.decode("utf-8", errors="replace") + result.check_returncode() + return result.stdout def own(prs, sha): - return {n: pr for n, pr in prs.items() - if pr["sha"] != sha and subprocess.run(["git", "merge-base", "--is-ancestor", pr["sha"], sha], capture_output=True).returncode != 0} + 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): @@ -79,11 +84,12 @@ def fetch(base, prs): def merge(*args): - result = subprocess.run(["git", "-c", "core.quotePath=false", "merge-tree", "--write-tree", "--name-only", "--no-messages", *args], - capture_output=True, text=True, encoding="utf-8", errors="replace") - lines = result.stdout.splitlines() - if result.returncode not in (0, 1) or not lines: - raise RuntimeError(f"git merge-tree {' '.join(args)} failed: {result.stderr.strip()}") + result = subprocess.run(["git", "-c", "core.quotePath=false", "merge-tree", "--write-tree", "--name-only", "--no-messages", "-z", *args], + capture_output=True) + lines = result.stdout.decode("utf-8", errors="replace").rstrip("\0").split("\0") + if result.returncode not in (0, 1) or not lines[0]: + error = result.stderr.decode("utf-8", errors="replace").strip() + raise RuntimeError(f"git merge-tree {' '.join(args)} failed: {error}") return lines[0], set(lines[1:]) if result.returncode else set() @@ -115,8 +121,8 @@ def scan(pr): pr["tree"] = None if conflicted else commit(tree) old, new = (git("merge-base", "refs/pr/base", pr["ref"]).strip(), pr["ref"]) if conflicted else ("refs/pr/base", pr["tree"]) pr["files"], pr["added"] = set(), set() - for line in git("diff", "--name-status", "--no-renames", old, new).splitlines(): - status, path = line.split("\t", 1) + changes = git("diff", "--name-status", "--no-renames", "-z", old, new).split("\0") + for status, path in zip(changes[::2], changes[1::2]): pr["files"].add(path) if status == "A": pr["added"].add(path) @@ -124,6 +130,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,7 +195,7 @@ 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())) @@ -236,9 +247,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) @@ -246,8 +260,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/stubgen.py b/tools/stubgen.py index 2d451278cc..ae9f68955b 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -2,6 +2,7 @@ import argparse import os import re +import stat import sys import tempfile from pathlib import Path @@ -194,6 +195,10 @@ def write_outputs(outputs): temporary.chmod(path.stat().st_mode & 0o7777) else: temporary.chmod(0o666 & ~umask) + if os.name == "nt": + for _, path in staged: + if path.exists() and path.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: + raise PermissionError(f"Output is read-only: {path}") for temporary, path in staged: temporary.replace(path) finally: 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..3a56e49429 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -24,6 +24,44 @@ 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 test_file_ancestors_are_rejected_before_publication(self): + valid = library_zip([("libs/libc.prx", b"valid")]) + for entries in (("libs/nested", "libs/nested/libc.prx"), + ("libs/NESTED/libc.prx", "libs/nested")): + with self.subTest(entries=entries): + invalid = library_zip([(name, b"content") for name in entries]) + with self.assertRaisesRegex(ValueError, "file is also used as a directory"): + self.publish(invalid, valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + + def test_explicit_and_implicit_archive_directories_are_valid(self): + for entries in ([('libs/sub/libc.prx', b'content')], + [('libs/sub/', b''), ('libs/sub/libc.prx', b'content')]): + with self.subTest(entries=entries): + payload = library_zip(entries) + self.publish(payload, payload) + self.assertEqual(json.loads(self.manifest.read_text(encoding="utf-8"))["tag"], "v1") + + 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..3b83764f0a 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) @@ -27,7 +61,7 @@ class CheckCompileTests(unittest.TestCase): environment = dict(os.environ) environment.update(env or {}) return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, - capture_output=True, text=True, env=environment) + capture_output=True, text=True, encoding="utf-8", env=environment) def source(self, relative, text="int x;\n"): path = self.root / relative @@ -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) @@ -170,6 +289,28 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 1) self.assertIn("changed.cpp:1:1: error: no", result.stderr) + def test_files_changed_only_on_the_base_since_the_branch_point_are_not_checked(self): + script = self.compiler("import sys\nsys.stderr.write(sys.argv[-1] + ': error: no\\n')\nsys.exit(1)\n") + self.manifest([self.entry(name, f'"{sys.executable}" "{script}" "{self.root / name}"') + for name in ("mine.cpp", "other.cpp")]) + git = ["git", "-C", str(self.root)] + self.source("other.cpp") + subprocess.run([*git, "add", "other.cpp"], check=True) + subprocess.run([*git, "commit", "-qm", "add"], check=True) + subprocess.run([*git, "branch", "-M", "main"], check=True) + subprocess.run([*git, "checkout", "-qb", "side"], check=True) + self.source("mine.cpp") + subprocess.run([*git, "add", "mine.cpp"], check=True) + subprocess.run([*git, "commit", "-qm", "mine"], check=True) + subprocess.run([*git, "checkout", "-q", "main"], check=True) + self.source("other.cpp", "int y;\n") + subprocess.run([*git, "commit", "-qam", "other"], check=True) + subprocess.run([*git, "checkout", "-q", "side"], check=True) + result = self.tool("--base", "main") + self.assertEqual(result.returncode, 1) + self.assertIn("mine.cpp: error: no", result.stderr) + self.assertNotIn("other.cpp", result.stderr) + def test_no_changed_translation_unit_is_not_a_failure(self): self.source("clean.cpp") subprocess.run(["git", "-C", str(self.root), "add", "clean.cpp"], check=True) @@ -198,5 +339,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_comments.py b/tools/tests/test_check_conventions_comments.py index b7f46f54f8..7a1004b5c0 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -11,7 +11,7 @@ from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output -CONVENTIONS_TEST_SHARDS = 6 +CONVENTIONS_TEST_SHARDS = 12 def iter_test_cases(suite): @@ -157,6 +157,54 @@ class RawLiteralCommentTests(unittest.TestCase): self.assertEqual(self.findings(comment, [1]), [("comment", self.path, 1, "")]) +class HashCommentTests(unittest.TestCase): + def findings(self, path, source, added): + check = Check.__new__(Check) + check.tree = set() + check.status = {} + check.findings = [] + lines = source.split("\n") + check.added = {path: [(number, lines[number - 1]) for number in added]} + with patch.object(check, "show", return_value=source), patch.object(check, "duplicates"): + check.code() + return check.findings + + def test_hash_inside_python_multiline_string_is_not_a_comment(self): + for quote in ('"""', "'''"): + with self.subTest(quote=quote): + source = f'TEMPLATE = {quote}\n#include \n{quote}\nprint("#ok")\n' + self.assertEqual(self.findings("tools/generate.py", source, [1, 2, 3, 4]), []) + + def test_python_comment_after_a_multiline_string_is_reported(self): + source = 'TEMPLATE = """\n#include \n""" # pending\nvalue = 1 # pending\n' + self.assertEqual(self.findings("tools/generate.py", source, [2, 3, 4]), + [("comment", "tools/generate.py", 3, ""), ("comment", "tools/generate.py", 4, "")]) + + def test_python_shebang_is_not_a_comment(self): + for source in ("#!/usr/bin/env python3\nprint(1)\n", "\ufeff#!/usr/bin/env python3\nprint(1)\n"): + with self.subTest(source=source): + self.assertEqual(self.findings("tools/generate.py", source, [1, 2]), []) + + def test_unparsable_python_falls_back_to_the_line_check(self): + source = 'TEMPLATE = """\n#include \n' + self.assertEqual(self.findings("tools/generate.py", source, [2]), [("comment", "tools/generate.py", 2, "")]) + + def test_hash_inside_cmake_quoted_and_bracket_arguments_is_not_a_comment(self): + for source in ('set(content "#ifndef GUARD\n#define GUARD\n#endif\n")\n', + 'set(patch [=[\n#include \n]=])\n', + 'set(patch [[\n#include \n]])\n', + 'message(STATUS "a\\"#b")\n', + 'string(REPLACE \\# x out "${in}")\n'): + with self.subTest(source=source): + added = list(range(1, source.count("\n") + 1)) + self.assertEqual(self.findings("cmake/Generate.cmake", source, added), []) + + def test_cmake_line_and_bracket_comments_are_reported(self): + source = 'set(value 1) # pending\n#[[\npending\n]]\nset(patch [=[\n#include \n]=]) # pending\n' + self.assertEqual(self.findings("CMakeLists.txt", source, [1, 2, 3, 4, 6, 7]), + [("comment", "CMakeLists.txt", number, "") for number in (1, 2, 3, 4, 7)]) + + class SilentStubTests(unittest.TestCase): def test_commented_stub_is_reported_when_debt_change_allows_comments(self): checker = Path(__file__).resolve().parents[1] / "check_conventions.py" @@ -345,7 +393,8 @@ class SilentStubSectionTests(unittest.TestCase): class TestFixtureExportTests(unittest.TestCase): - def check(self, path, text): + def check(self, path, text=None): + files = path if text is None else {path: text} checker = Path(__file__).resolve().parents[1] / "check_conventions.py" with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -367,7 +416,8 @@ class TestFixtureExportTests(unittest.TestCase): write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") git("add", ".") git("commit", "-qm", "feat: add libraries") - write(path, text) + for relative, content in files.items(): + write(relative, content) git("add", ".") git("commit", "-qm", "feat: add source") env = {key: value for key, value in os.environ.items() @@ -396,12 +446,16 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) def test_comment_in_a_shader_is_reported(self): + shaders = {} for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag", "core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.glsl"): - for line in (" // fill the target", " /* fill the target */"): - with self.subTest(path=path, line=line): - result = self.check(path, f"#version 450\nvoid main() {{\n{line}\n}}\n") - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn(f"{path}:3: error: [comment]", result.stdout) + for style, line in (("Line", " // fill the target"), ("Block", " /* fill the target */")): + stem, extension = path.rsplit(".", 1) + shaders[f"{stem}{style}.{extension}"] = f"#version 450\nvoid main() {{\n{line}\n}}\n" + result = self.check(shaders) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + for path in shaders: + with self.subTest(path=path): + self.assertIn(f"{path}:3: error: [comment]", result.stdout) def test_shader_without_comments_passes(self): result = self.check("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.vert", 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..62c6f9642c 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) @@ -58,6 +63,39 @@ class WrappedDeclarationTests(unittest.TestCase): self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("[duplicate-export]", result.stdout) + def test_multiline_raw_literal_is_not_a_duplicate(self): + source = 'const char* example = R"example(\n' + DEFINITION + ')example";\n' + result = self.check("Export.cpp", source) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_block_comment_is_not_a_duplicate(self): + result = self.check("Export.cpp", "/*\n" + DEFINITION + "*/\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_literal_before_unrelated_export_is_not_a_duplicate(self): + source = 'const char* example = "APS5_VABI sceFooBar()"; int APS5_VABI sceOther() { return 1; }\n' + result = self.check("Export.cpp", source) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_line_comment_before_definition_is_not_a_duplicate(self): + result = self.check("Export.cpp", "// " + DEFINITION + "int unrelated() { return 42; }\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_definition_after_unicode_raw_literal_is_a_duplicate(self): + source = 'const char* text = R"text(first\u2028+middle\u2029+last)text";\n' + DEFINITION + result = self.check("Export.cpp", source) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[duplicate-export]", result.stdout) + + def test_definition_after_commented_example_is_a_duplicate(self): + result = self.check("Export.cpp", "/*\n" + DEFINITION + "*/\n" + DEFINITION) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(result.stdout.count("[duplicate-export]"), 1, result.stdout) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index 4a13a56e0a..d893e70b0e 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -177,6 +177,14 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual(result.returncode, 2) self.assertIn("no base ref found", result.stderr) + def test_explicit_base_is_fetched_before_it_is_resolved(self): + with tempfile.TemporaryDirectory() as other: + subprocess.run(["git", "clone", "-q", self.directory.name, other], check=True, capture_output=True) + self.repo.git("remote", "add", "upstream", other) + result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--base", "upstream/main", "--fetch"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("merge: clean", result.stdout) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 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..85d27593e0 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")) @@ -1291,7 +1613,7 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, r"entry 0 needs string fields"): import_audit.read_registry(path) write_registry(path, [("AAAAAAAAAAA", ""), ("bad\x01nid", "")]) - with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid NID 'bad\\x01nid'"): + with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid import symbol 'bad\\x01nid'"): import_audit.read_registry(path) write_registry(path, [("AAAAAAAAAAA#A#B", "libSceA.prx")], encoding="utf-8-sig") self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)]) @@ -1325,10 +1647,10 @@ class ImportAuditTests(unittest.TestCase): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" - write_registry(registry, [("short", "libSceA.prx")]) + write_registry(registry, [("bad name", "libSceA.prx")]) code, out, err = work.run(registry) self.assertEqual((code, out), (2, "")) - self.assertIn("game.json: entry 0 has an invalid NID 'short'", err) + self.assertIn("game.json: entry 0 has an invalid import symbol 'bad name'", err) def test_unreadable_library_and_unwritable_json_exit_two(self): work = self.work 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 dad871a5e2..520620c8a3 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -8,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 @@ -227,6 +228,45 @@ class ConflictTests(unittest.TestCase): class UnicodePathTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "POSIX filenames containing control characters") + def test_control_characters_keep_exact_paths_in_scans_and_conflicts(self): + previous = os.getcwd() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=root, + capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() + filenames = {"core/libs/prx/libExample/return\r.cpp", "core/libs/prx/libExample/tab\t.cpp", "core/libs/prx/libExample/line\n.cpp", + "core/libs/prx/libExample/quote\".cpp"} + for filename in filenames: + source = root / filename + source.parent.mkdir(parents=True, exist_ok=True) + source.write_text("int value = 0;\n", encoding="utf-8") + git("init", "-q") + git("add", ".") + git("commit", "-qm", "test: create base") + base = git("rev-parse", "HEAD") + git("update-ref", "refs/pr/base", base) + for branch, value in (("left", 1), ("right", 2)): + git("checkout", "-qb", branch, base) + for filename in filenames: + (root / filename).write_text(f"int value = {value};\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "fix: change value") + try: + os.chdir(root) + left, right = {"ref": "left"}, {"ref": "right"} + pr_overlap.scan(left) + pr_overlap.scan(right) + self.assertEqual(left["files"], filenames) + self.assertEqual(right["files"], filenames) + self.assertEqual(pr_overlap.conflicts(left, right), {name: [(1, 1)] for name in filenames}) + finally: + os.chdir(previous) + + def test_scans_and_reports_conflicts_with_git_path_quoting_enabled(self): previous = os.getcwd() with tempfile.TemporaryDirectory() as directory: @@ -238,7 +278,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") @@ -261,6 +301,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) @@ -292,8 +339,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([ @@ -302,7 +349,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_encoding.py b/tools/tests/test_pr_overlap_encoding.py index c8c04aff4d..1dad26c746 100644 --- a/tools/tests/test_pr_overlap_encoding.py +++ b/tools/tests/test_pr_overlap_encoding.py @@ -81,11 +81,11 @@ class EncodingTests(unittest.TestCase): def test_merge_conflict_preserves_unicode_paths(self): path = UNICODE + ".txt" - with command_output(TREE + "\n" + path + "\n", returncode=1): + with command_output(TREE + "\0" + path + "\0", returncode=1): self.assertEqual(pr_overlap.merge("main", "topic"), (TREE, {path})) def test_merge_success(self): - with command_output(TREE + "\n"): + with command_output(TREE + "\0"): self.assertEqual(pr_overlap.merge("main", "topic"), (TREE, set())) def test_merge_error_preserves_unicode(self): 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_stubgen.py b/tools/tests/test_stubgen.py index 3a8e9a8be8..bc4bf37488 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -41,6 +41,31 @@ class StubgenTests(unittest.TestCase): class StubGeneratorTests(unittest.TestCase): + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") + def test_readonly_second_output_preserves_both_outputs(self): + import subprocess + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids = root / "nids.txt" + code = root / "Export.cpp" + debt = root / "debt.md" + nids.write_text("AC1FtjqMCL0\n", encoding="utf-8") + code.write_bytes(b"original code\n") + debt.write_bytes(b"original debt\n") + debt.chmod(stat.S_IREAD) + try: + result = subprocess.run( + [sys.executable, str(Path(stubgen.__file__)), "--module", "libSceTest", + "--nids", str(nids), "--out", str(code), "--td-out", str(debt)], + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(code.read_bytes(), b"original code\n") + self.assertEqual(debt.read_bytes(), b"original debt\n") + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "debt.md", "nids.txt"]) + finally: + debt.chmod(stat.S_IREAD | stat.S_IWRITE) + def test_default_stdout_contains_only_cpp(self): with tempfile.TemporaryDirectory() as directory: nids = Path(directory) / "nids.txt" 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..5864781302 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"] @@ -188,6 +232,43 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(presenter["wait_ms_per_frame"], 15.5 / 3) self.assertAlmostEqual(presenter["busy_ms_per_frame"], 2.5 / 3) + def test_thread_totals_only_count_time_inside_the_frame_window(self): + events = [ + complete(1, "Driver.Packet", -5000, 20000), + complete(1, "GpuMutex.wait.draw", -2000, 4000), + complete(1, "Driver.Packet.after", 12000, 1000), + complete(2, "Driver.WorkerWait", -10000, 2000), + {"name": "present", "ph": "i", "ts": 0, "tid": 1, "args": {"present": 600}}, + {"name": "present", "ph": "i", "ts": 10000, "tid": 1, "args": {"present": 601}}, + ] + result = trace_digest.digest(trace_digest.Trace(events)) + active, outside = result["threads"] + self.assertEqual(active["busy_ms_per_frame"], 8.0) + self.assertEqual(active["wait_ms_per_frame"], 2.0) + self.assertEqual(active["busy_pct"], 80.0) + self.assertEqual(active["wait_pct"], 20.0) + self.assertEqual(active["top"], [{"name": "Driver.Packet", "ms_per_frame": 10.0}]) + self.assertEqual(outside["busy_ms_per_frame"], 0.0) + self.assertEqual(outside["wait_ms_per_frame"], 0.0) + self.assertEqual(outside["untraced_ms_per_frame"], 10.0) + self.assertEqual(outside["top"], []) + self.assertEqual(result["top_slices_ms_per_frame"], [ + {"name": "Driver.Packet", "ms": 10.0}, + {"name": "GpuMutex.wait.draw", "ms": 2.0}, + ]) + + def test_thread_totals_use_the_summary_end_after_the_last_present(self): + trace = trace_digest.Trace([ + complete(1, "Driver.Packet", 1000, 20000), + {"name": "present", "ph": "i", "ts": 0, "tid": 1, "args": {"present": 600}}, + ]) + result = trace_digest.digest(trace, {"window": {"ms": 10.0}}) + thread = result["threads"][0] + self.assertEqual(result["frames"], 1) + self.assertEqual(thread["busy_ms_per_frame"], 9.0) + self.assertEqual(thread["busy_pct"], 90.0) + self.assertEqual(thread["untraced_ms_per_frame"], 1.0) + def test_largest_slices_per_frame(self): largest = self.result()["top_slices_ms_per_frame"] self.assertEqual(largest[0]["name"], "Driver.Packet") @@ -234,6 +315,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 +367,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): @@ -318,6 +433,15 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual([(entry[3], entry[4]) for entry in trace.slices[1]], [(0, False), (1, True), (0, False)]) + def test_missing_summary_file_is_an_error(self): + missing = Path(self.directory.name) / "missing.jsonl" + error = io.StringIO() + with contextlib.redirect_stderr(error), contextlib.redirect_stdout(io.StringIO()): + with self.assertRaises(SystemExit) as raised: + trace_digest.main([str(self.trace), "--summary", str(missing)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("summary file not found", error.getvalue()) + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, @@ -377,5 +501,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(" 0: + totals[name] += overlap + if depth == 0: + traced += overlap + if not is_wait(name): + leaders[name] += overlap + if is_wait(name) and not nested: + waited += overlap if is_wait(name) and not nested: waits.append((dur, name, trace.name(tid), ts)) threads.append({"thread": trace.name(tid), "busy_ms_per_frame": (traced - waited) / 1000.0 / count, @@ -188,6 +203,10 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): return result +def table_cell(value): + return html.escape(str(value)).replace("|", "|").replace("\r\n", "\n").replace("\r", "\n").replace("\n", "
") + + def markdown(result): lines = [] summary = result["summary"] @@ -217,8 +236,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 +308,41 @@ 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.summary and not Path(options.summary).is_file(): + parser.error(f"summary file not found: {options.summary}") 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 +350,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("